Skip to content

Commit a96ca03

Browse files
committed
feat(ashby): add webhook triggers with automatic lifecycle management
1 parent c939f8a commit a96ca03

File tree

17 files changed

+788
-3
lines changed

17 files changed

+788
-3
lines changed

apps/docs/content/docs/en/tools/ashby.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ With Ashby, you can:
2222
- **List and view jobs**: Browse all open, closed, and archived job postings with location and department info
2323
- **List applications**: View all applications across your organization with candidate and job details, status tracking, and pagination
2424

25+
The Ashby block also supports **webhook triggers** that automatically start workflows in response to Ashby events. Available triggers include Application Submitted, Candidate Stage Change, Candidate Hired, Candidate Deleted, Job Created, Offer Created, and a Generic Webhook for all events. Webhooks are fully managed — Sim automatically creates the webhook in Ashby when you save the trigger and deletes it when you remove it, so there's no manual webhook configuration needed. Just provide your Ashby API key (with `apiKeysWrite` permission) and select the event type.
26+
2527
In Sim, the Ashby integration enables your agents to programmatically manage your recruiting pipeline. Agents can search for candidates, create new candidate records, add notes after interviews, and monitor applications across jobs. This allows you to automate recruiting workflows like candidate intake, interview follow-ups, pipeline reporting, and cross-referencing candidates across roles.
2628
{/* MANUAL-CONTENT-END */}
2729

apps/docs/content/docs/en/tools/evernote.mdx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
1010
color="#E0E0E0"
1111
/>
1212

13+
{/* MANUAL-CONTENT-START:intro */}
14+
[Evernote](https://evernote.com/) is a note-taking and organization platform that helps individuals and teams capture ideas, manage projects, and store information across devices. With notebooks, tags, and powerful search, Evernote serves as a central hub for knowledge management.
15+
16+
With the Sim Evernote integration, you can:
17+
18+
- **Create and update notes**: Programmatically create new notes with content and tags, or update existing notes in any notebook.
19+
- **Search and retrieve notes**: Use Evernote's search grammar to find notes by keyword, tag, notebook, or other criteria, and retrieve full note content.
20+
- **Organize with notebooks and tags**: Create notebooks and tags, list existing ones, and move or copy notes between notebooks.
21+
- **Delete and manage notes**: Move notes to trash or copy them to different notebooks as part of automated workflows.
22+
23+
**How it works in Sim:**
24+
Add an Evernote block to your workflow and select an operation (e.g., create note, search notes, list notebooks). Provide your Evernote developer token and any required parameters. The block calls the Evernote API and returns structured data you can pass to downstream blocks — for example, searching for meeting notes and sending summaries to Slack, or creating notes from AI-generated content.
25+
{/* MANUAL-CONTENT-END */}
26+
27+
1328
## Usage Instructions
1429

1530
Integrate with Evernote to manage notes, notebooks, and tags. Create, read, update, copy, search, and delete notes. Create and list notebooks and tags.

apps/docs/content/docs/en/tools/fathom.mdx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
1010
color="#181C1E"
1111
/>
1212

13+
{/* MANUAL-CONTENT-START:intro */}
14+
[Fathom](https://fathom.video/) is an AI meeting assistant that automatically records, transcribes, and summarizes your video calls. It works across platforms like Zoom, Google Meet, and Microsoft Teams, generating highlights and action items so your team can stay focused during meetings and catch up quickly afterward.
15+
16+
With the Sim Fathom integration, you can:
17+
18+
- **List and filter meetings**: Retrieve recent meetings recorded by you or shared with your team, with optional filters by date range, recorder, or team.
19+
- **Get meeting summaries**: Pull structured, markdown-formatted summaries for any recorded meeting to quickly review key discussion points.
20+
- **Access full transcripts**: Retrieve complete transcripts with speaker attribution and timestamps for detailed review or downstream processing.
21+
- **Manage teams and members**: List teams in your Fathom organization and view team member details to coordinate meeting workflows.
22+
23+
**How it works in Sim:**
24+
Add a Fathom block to your workflow and select an operation. Provide your Fathom API key and any required parameters (such as a recording ID for summaries and transcripts). The block calls the Fathom API and returns structured data you can pass to downstream blocks — for example, sending a summary to Slack or extracting action items with an AI agent.
25+
{/* MANUAL-CONTENT-END */}
26+
27+
1328
## Usage Instructions
1429

1530
Integrate Fathom AI Notetaker into your workflow. List meetings, get transcripts and summaries, and manage team members and teams. Can also trigger workflows when new meeting content is ready.

apps/docs/content/docs/en/tools/obsidian.mdx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
1010
color="#0F0F0F"
1111
/>
1212

13+
{/* MANUAL-CONTENT-START:intro */}
14+
[Obsidian](https://obsidian.md/) is a powerful knowledge base and note-taking application that works on top of a local folder of plain-text Markdown files. With features like bidirectional linking, graph views, and a rich plugin ecosystem, Obsidian is widely used for personal knowledge management, research, and documentation.
15+
16+
With the Sim Obsidian integration, you can:
17+
18+
- **Read and create notes**: Retrieve note content from your vault or create new notes programmatically as part of automated workflows.
19+
- **Update and patch notes**: Modify existing notes in full or patch content at specific locations within a note.
20+
- **Search your vault**: Find notes by keyword or content across your entire Obsidian vault.
21+
- **Manage periodic notes**: Access and create daily or other periodic notes for journaling and task tracking.
22+
- **Execute commands**: Trigger Obsidian commands remotely to automate vault operations.
23+
24+
**How it works in Sim:**
25+
Add an Obsidian block to your workflow and select an operation. This integration requires the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin to be installed and running in your vault. Provide your API key and vault URL, along with any required parameters. The block communicates with your local Obsidian instance and returns structured data you can pass to downstream blocks — for example, searching your vault for research notes and feeding them into an AI agent for summarization.
26+
{/* MANUAL-CONTENT-END */}
27+
28+
1329
## Usage Instructions
1430

1531
Read, create, update, search, and delete notes in your Obsidian vault. Manage periodic notes, execute commands, and patch content at specific locations. Requires the Obsidian Local REST API plugin.

apps/sim/blocks/blocks/ashby.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AshbyIcon } from '@/components/icons'
22
import { AuthMode, type BlockConfig } from '@/blocks/types'
3+
import { getTrigger } from '@/triggers'
34

45
export const AshbyBlock: BlockConfig = {
56
type: 'ashby',
@@ -13,6 +14,19 @@ export const AshbyBlock: BlockConfig = {
1314
icon: AshbyIcon,
1415
authMode: AuthMode.ApiKey,
1516

17+
triggers: {
18+
enabled: true,
19+
available: [
20+
'ashby_application_submit',
21+
'ashby_candidate_stage_change',
22+
'ashby_candidate_hire',
23+
'ashby_candidate_delete',
24+
'ashby_job_create',
25+
'ashby_offer_create',
26+
'ashby_webhook',
27+
],
28+
},
29+
1630
subBlocks: [
1731
{
1832
id: 'operation',
@@ -145,7 +159,6 @@ export const AshbyBlock: BlockConfig = {
145159
type: 'short-input',
146160
placeholder: 'Updated full name',
147161
condition: { field: 'operation', value: 'update_candidate' },
148-
mode: 'advanced',
149162
},
150163
{
151164
id: 'websiteUrl',
@@ -366,6 +379,15 @@ Output only the ISO 8601 timestamp string, nothing else.`,
366379
},
367380
mode: 'advanced',
368381
},
382+
383+
// Trigger subBlocks
384+
...getTrigger('ashby_application_submit').subBlocks,
385+
...getTrigger('ashby_candidate_stage_change').subBlocks,
386+
...getTrigger('ashby_candidate_hire').subBlocks,
387+
...getTrigger('ashby_candidate_delete').subBlocks,
388+
...getTrigger('ashby_job_create').subBlocks,
389+
...getTrigger('ashby_offer_create').subBlocks,
390+
...getTrigger('ashby_webhook').subBlocks,
369391
],
370392

371393
tools: {

apps/sim/lib/webhooks/provider-subscriptions.ts

Lines changed: 177 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const telegramLogger = createLogger('TelegramWebhook')
1616
const airtableLogger = createLogger('AirtableWebhook')
1717
const typeformLogger = createLogger('TypeformWebhook')
1818
const calendlyLogger = createLogger('CalendlyWebhook')
19+
const ashbyLogger = createLogger('AshbyWebhook')
1920
const grainLogger = createLogger('GrainWebhook')
2021
const fathomLogger = createLogger('FathomWebhook')
2122
const lemlistLogger = createLogger('LemlistWebhook')
@@ -1974,6 +1975,7 @@ type RecreateCheckInput = {
19741975
/** Providers that create external webhook subscriptions */
19751976
const PROVIDERS_WITH_EXTERNAL_SUBSCRIPTIONS = new Set([
19761977
'airtable',
1978+
'ashby',
19771979
'attio',
19781980
'calendly',
19791981
'fathom',
@@ -2046,7 +2048,13 @@ export async function createExternalWebhookSubscription(
20462048
let updatedProviderConfig = providerConfig
20472049
let externalSubscriptionCreated = false
20482050

2049-
if (provider === 'airtable') {
2051+
if (provider === 'ashby') {
2052+
const result = await createAshbyWebhookSubscription(webhookData, requestId)
2053+
if (result) {
2054+
updatedProviderConfig = { ...updatedProviderConfig, externalId: result.id }
2055+
externalSubscriptionCreated = true
2056+
}
2057+
} else if (provider === 'airtable') {
20502058
const externalId = await createAirtableWebhookSubscription(userId, webhookData, requestId)
20512059
if (externalId) {
20522060
updatedProviderConfig = { ...updatedProviderConfig, externalId }
@@ -2126,7 +2134,9 @@ export async function cleanupExternalWebhook(
21262134
workflow: any,
21272135
requestId: string
21282136
): Promise<void> {
2129-
if (webhook.provider === 'airtable') {
2137+
if (webhook.provider === 'ashby') {
2138+
await deleteAshbyWebhook(webhook, requestId)
2139+
} else if (webhook.provider === 'airtable') {
21302140
await deleteAirtableWebhook(webhook, workflow, requestId)
21312141
} else if (webhook.provider === 'attio') {
21322142
await deleteAttioWebhook(webhook, workflow, requestId)
@@ -2148,3 +2158,168 @@ export async function cleanupExternalWebhook(
21482158
await deleteLemlistWebhook(webhook, requestId)
21492159
}
21502160
}
2161+
2162+
/**
2163+
* Creates a webhook subscription in Ashby via webhook.create API.
2164+
* Ashby uses Basic Auth and one webhook per event type (webhookType).
2165+
*/
2166+
export async function createAshbyWebhookSubscription(
2167+
webhookData: any,
2168+
requestId: string
2169+
): Promise<{ id: string } | undefined> {
2170+
try {
2171+
const { path, providerConfig } = webhookData
2172+
const { apiKey, triggerId } = providerConfig || {}
2173+
2174+
if (!apiKey) {
2175+
ashbyLogger.warn(`[${requestId}] Missing apiKey for Ashby webhook creation.`, {
2176+
webhookId: webhookData.id,
2177+
})
2178+
throw new Error(
2179+
'Ashby API Key is required. Please provide your API Key with apiKeysWrite permission in the trigger configuration.'
2180+
)
2181+
}
2182+
2183+
if (!triggerId) {
2184+
ashbyLogger.warn(`[${requestId}] Missing triggerId for Ashby webhook creation.`, {
2185+
webhookId: webhookData.id,
2186+
})
2187+
throw new Error('Trigger ID is required to create Ashby webhook.')
2188+
}
2189+
2190+
const webhookTypeMap: Record<string, string | undefined> = {
2191+
ashby_application_submit: 'applicationSubmit',
2192+
ashby_candidate_stage_change: 'candidateStageChange',
2193+
ashby_candidate_hire: 'candidateHire',
2194+
ashby_candidate_delete: 'candidateDelete',
2195+
ashby_job_create: 'jobCreate',
2196+
ashby_offer_create: 'offerCreate',
2197+
ashby_webhook: undefined,
2198+
}
2199+
2200+
const webhookType = webhookTypeMap[triggerId]
2201+
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
2202+
const authString = Buffer.from(`${apiKey}:`).toString('base64')
2203+
2204+
ashbyLogger.info(`[${requestId}] Creating Ashby webhook`, {
2205+
triggerId,
2206+
webhookType,
2207+
webhookId: webhookData.id,
2208+
})
2209+
2210+
const requestBody: Record<string, unknown> = {
2211+
requestUrl: notificationUrl,
2212+
}
2213+
2214+
if (webhookType) {
2215+
requestBody.webhookType = webhookType
2216+
}
2217+
2218+
const ashbyResponse = await fetch('https://api.ashbyhq.com/webhook.create', {
2219+
method: 'POST',
2220+
headers: {
2221+
Authorization: `Basic ${authString}`,
2222+
'Content-Type': 'application/json',
2223+
},
2224+
body: JSON.stringify(requestBody),
2225+
})
2226+
2227+
const responseBody = await ashbyResponse.json()
2228+
2229+
if (!ashbyResponse.ok || !responseBody.success) {
2230+
const errorMessage =
2231+
responseBody.errorInfo?.message || responseBody.message || 'Unknown Ashby API error'
2232+
ashbyLogger.error(
2233+
`[${requestId}] Failed to create webhook in Ashby for webhook ${webhookData.id}. Status: ${ashbyResponse.status}`,
2234+
{ message: errorMessage, response: responseBody }
2235+
)
2236+
2237+
let userFriendlyMessage = 'Failed to create webhook subscription in Ashby'
2238+
if (ashbyResponse.status === 401) {
2239+
userFriendlyMessage =
2240+
'Invalid Ashby API Key. Please verify your API Key is correct and has apiKeysWrite permission.'
2241+
} else if (ashbyResponse.status === 403) {
2242+
userFriendlyMessage =
2243+
'Access denied. Please ensure your Ashby API Key has the apiKeysWrite permission.'
2244+
} else if (errorMessage && errorMessage !== 'Unknown Ashby API error') {
2245+
userFriendlyMessage = `Ashby error: ${errorMessage}`
2246+
}
2247+
2248+
throw new Error(userFriendlyMessage)
2249+
}
2250+
2251+
const externalId = responseBody.results?.id
2252+
if (!externalId) {
2253+
ashbyLogger.error(
2254+
`[${requestId}] Ashby webhook created but no ID returned for webhook ${webhookData.id}`,
2255+
{ response: responseBody }
2256+
)
2257+
throw new Error('Ashby webhook creation succeeded but no webhook ID was returned')
2258+
}
2259+
2260+
ashbyLogger.info(
2261+
`[${requestId}] Successfully created Ashby webhook subscription ${externalId} for webhook ${webhookData.id}`
2262+
)
2263+
return { id: externalId }
2264+
} catch (error: any) {
2265+
ashbyLogger.error(
2266+
`[${requestId}] Exception during Ashby webhook creation for webhook ${webhookData.id}.`,
2267+
{
2268+
message: error.message,
2269+
stack: error.stack,
2270+
}
2271+
)
2272+
throw error
2273+
}
2274+
}
2275+
2276+
/**
2277+
* Deletes an Ashby webhook subscription via webhook.delete API.
2278+
* Ashby uses POST with webhookId in the body (not DELETE method).
2279+
*/
2280+
export async function deleteAshbyWebhook(webhook: any, requestId: string): Promise<void> {
2281+
try {
2282+
const config = getProviderConfig(webhook)
2283+
const apiKey = config.apiKey as string | undefined
2284+
const externalId = config.externalId as string | undefined
2285+
2286+
if (!apiKey) {
2287+
ashbyLogger.warn(
2288+
`[${requestId}] Missing apiKey for Ashby webhook deletion ${webhook.id}, skipping cleanup`
2289+
)
2290+
return
2291+
}
2292+
2293+
if (!externalId) {
2294+
ashbyLogger.warn(
2295+
`[${requestId}] Missing externalId for Ashby webhook deletion ${webhook.id}, skipping cleanup`
2296+
)
2297+
return
2298+
}
2299+
2300+
const authString = Buffer.from(`${apiKey}:`).toString('base64')
2301+
2302+
const ashbyResponse = await fetch('https://api.ashbyhq.com/webhook.delete', {
2303+
method: 'POST',
2304+
headers: {
2305+
Authorization: `Basic ${authString}`,
2306+
'Content-Type': 'application/json',
2307+
},
2308+
body: JSON.stringify({ webhookId: externalId }),
2309+
})
2310+
2311+
if (!ashbyResponse.ok && ashbyResponse.status !== 404) {
2312+
const responseBody = await ashbyResponse.json().catch(() => ({}))
2313+
ashbyLogger.warn(
2314+
`[${requestId}] Failed to delete Ashby webhook (non-fatal): ${ashbyResponse.status}`,
2315+
{ response: responseBody }
2316+
)
2317+
} else {
2318+
ashbyLogger.info(
2319+
`[${requestId}] Successfully deleted Ashby webhook subscription ${externalId}`
2320+
)
2321+
}
2322+
} catch (error) {
2323+
ashbyLogger.warn(`[${requestId}] Error deleting Ashby webhook (non-fatal)`, error)
2324+
}
2325+
}

apps/sim/lib/webhooks/utils.server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,14 @@ export async function formatWebhookInput(
12441244
return extractPageData(body)
12451245
}
12461246

1247+
if (foundWebhook.provider === 'ashby') {
1248+
return {
1249+
action: body.action,
1250+
...(body.data || {}),
1251+
data: body.data || {},
1252+
}
1253+
}
1254+
12471255
if (foundWebhook.provider === 'stripe') {
12481256
return body
12491257
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { AshbyIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
ashbySetupInstructions,
5+
ashbyTriggerOptions,
6+
buildApplicationSubmitOutputs,
7+
buildAshbyExtraFields,
8+
} from '@/triggers/ashby/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Ashby Application Submitted Trigger
13+
*
14+
* This is the PRIMARY trigger - it includes the dropdown for selecting trigger type.
15+
* Fires when a candidate submits an application or is manually added.
16+
*/
17+
export const ashbyApplicationSubmitTrigger: TriggerConfig = {
18+
id: 'ashby_application_submit',
19+
name: 'Ashby Application Submitted',
20+
provider: 'ashby',
21+
description: 'Trigger workflow when a new application is submitted',
22+
version: '1.0.0',
23+
icon: AshbyIcon,
24+
25+
subBlocks: buildTriggerSubBlocks({
26+
triggerId: 'ashby_application_submit',
27+
triggerOptions: ashbyTriggerOptions,
28+
includeDropdown: true,
29+
setupInstructions: ashbySetupInstructions('Application Submitted'),
30+
extraFields: buildAshbyExtraFields('ashby_application_submit'),
31+
}),
32+
33+
outputs: buildApplicationSubmitOutputs(),
34+
35+
webhook: {
36+
method: 'POST',
37+
headers: {
38+
'Content-Type': 'application/json',
39+
},
40+
},
41+
}

0 commit comments

Comments
 (0)