diff --git a/apps/docs/content/docs/en/tools/ashby.mdx b/apps/docs/content/docs/en/tools/ashby.mdx index 72c050beac..ac9c39d19a 100644 --- a/apps/docs/content/docs/en/tools/ashby.mdx +++ b/apps/docs/content/docs/en/tools/ashby.mdx @@ -22,6 +22,8 @@ With Ashby, you can: - **List and view jobs**: Browse all open, closed, and archived job postings with location and department info - **List applications**: View all applications across your organization with candidate and job details, status tracking, and pagination +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, and Offer Created. 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. + 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. {/* MANUAL-CONTENT-END */} diff --git a/apps/docs/content/docs/en/tools/evernote.mdx b/apps/docs/content/docs/en/tools/evernote.mdx index 4c024edea3..a54840288f 100644 --- a/apps/docs/content/docs/en/tools/evernote.mdx +++ b/apps/docs/content/docs/en/tools/evernote.mdx @@ -10,6 +10,21 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" color="#E0E0E0" /> +{/* MANUAL-CONTENT-START:intro */} +[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. + +With the Sim Evernote integration, you can: + +- **Create and update notes**: Programmatically create new notes with content and tags, or update existing notes in any notebook. +- **Search and retrieve notes**: Use Evernote's search grammar to find notes by keyword, tag, notebook, or other criteria, and retrieve full note content. +- **Organize with notebooks and tags**: Create notebooks and tags, list existing ones, and move or copy notes between notebooks. +- **Delete and manage notes**: Move notes to trash or copy them to different notebooks as part of automated workflows. + +**How it works in Sim:** +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. +{/* MANUAL-CONTENT-END */} + + ## Usage Instructions Integrate with Evernote to manage notes, notebooks, and tags. Create, read, update, copy, search, and delete notes. Create and list notebooks and tags. diff --git a/apps/docs/content/docs/en/tools/fathom.mdx b/apps/docs/content/docs/en/tools/fathom.mdx index 31b4988663..28984cb9c1 100644 --- a/apps/docs/content/docs/en/tools/fathom.mdx +++ b/apps/docs/content/docs/en/tools/fathom.mdx @@ -10,6 +10,21 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" color="#181C1E" /> +{/* MANUAL-CONTENT-START:intro */} +[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. + +With the Sim Fathom integration, you can: + +- **List and filter meetings**: Retrieve recent meetings recorded by you or shared with your team, with optional filters by date range, recorder, or team. +- **Get meeting summaries**: Pull structured, markdown-formatted summaries for any recorded meeting to quickly review key discussion points. +- **Access full transcripts**: Retrieve complete transcripts with speaker attribution and timestamps for detailed review or downstream processing. +- **Manage teams and members**: List teams in your Fathom organization and view team member details to coordinate meeting workflows. + +**How it works in Sim:** +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. +{/* MANUAL-CONTENT-END */} + + ## Usage Instructions 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. diff --git a/apps/docs/content/docs/en/tools/obsidian.mdx b/apps/docs/content/docs/en/tools/obsidian.mdx index c2b28f74cb..e15f6f9f9f 100644 --- a/apps/docs/content/docs/en/tools/obsidian.mdx +++ b/apps/docs/content/docs/en/tools/obsidian.mdx @@ -10,6 +10,22 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" color="#0F0F0F" /> +{/* MANUAL-CONTENT-START:intro */} +[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. + +With the Sim Obsidian integration, you can: + +- **Read and create notes**: Retrieve note content from your vault or create new notes programmatically as part of automated workflows. +- **Update and patch notes**: Modify existing notes in full or patch content at specific locations within a note. +- **Search your vault**: Find notes by keyword or content across your entire Obsidian vault. +- **Manage periodic notes**: Access and create daily or other periodic notes for journaling and task tracking. +- **Execute commands**: Trigger Obsidian commands remotely to automate vault operations. + +**How it works in Sim:** +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. +{/* MANUAL-CONTENT-END */} + + ## Usage Instructions 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. diff --git a/apps/sim/blocks/blocks/ashby.ts b/apps/sim/blocks/blocks/ashby.ts index 642ffc866d..409c453603 100644 --- a/apps/sim/blocks/blocks/ashby.ts +++ b/apps/sim/blocks/blocks/ashby.ts @@ -1,5 +1,6 @@ import { AshbyIcon } from '@/components/icons' import { AuthMode, type BlockConfig } from '@/blocks/types' +import { getTrigger } from '@/triggers' export const AshbyBlock: BlockConfig = { type: 'ashby', @@ -13,6 +14,18 @@ export const AshbyBlock: BlockConfig = { icon: AshbyIcon, authMode: AuthMode.ApiKey, + triggers: { + enabled: true, + available: [ + 'ashby_application_submit', + 'ashby_candidate_stage_change', + 'ashby_candidate_hire', + 'ashby_candidate_delete', + 'ashby_job_create', + 'ashby_offer_create', + ], + }, + subBlocks: [ { id: 'operation', @@ -366,6 +379,14 @@ Output only the ISO 8601 timestamp string, nothing else.`, }, mode: 'advanced', }, + + // Trigger subBlocks + ...getTrigger('ashby_application_submit').subBlocks, + ...getTrigger('ashby_candidate_stage_change').subBlocks, + ...getTrigger('ashby_candidate_hire').subBlocks, + ...getTrigger('ashby_candidate_delete').subBlocks, + ...getTrigger('ashby_job_create').subBlocks, + ...getTrigger('ashby_offer_create').subBlocks, ], tools: { diff --git a/apps/sim/lib/webhooks/provider-subscriptions.ts b/apps/sim/lib/webhooks/provider-subscriptions.ts index f220f33220..dbab9c6646 100644 --- a/apps/sim/lib/webhooks/provider-subscriptions.ts +++ b/apps/sim/lib/webhooks/provider-subscriptions.ts @@ -16,6 +16,7 @@ const telegramLogger = createLogger('TelegramWebhook') const airtableLogger = createLogger('AirtableWebhook') const typeformLogger = createLogger('TypeformWebhook') const calendlyLogger = createLogger('CalendlyWebhook') +const ashbyLogger = createLogger('AshbyWebhook') const grainLogger = createLogger('GrainWebhook') const fathomLogger = createLogger('FathomWebhook') const lemlistLogger = createLogger('LemlistWebhook') @@ -1974,6 +1975,7 @@ type RecreateCheckInput = { /** Providers that create external webhook subscriptions */ const PROVIDERS_WITH_EXTERNAL_SUBSCRIPTIONS = new Set([ 'airtable', + 'ashby', 'attio', 'calendly', 'fathom', @@ -2046,7 +2048,13 @@ export async function createExternalWebhookSubscription( let updatedProviderConfig = providerConfig let externalSubscriptionCreated = false - if (provider === 'airtable') { + if (provider === 'ashby') { + const result = await createAshbyWebhookSubscription(webhookData, requestId) + if (result) { + updatedProviderConfig = { ...updatedProviderConfig, externalId: result.id } + externalSubscriptionCreated = true + } + } else if (provider === 'airtable') { const externalId = await createAirtableWebhookSubscription(userId, webhookData, requestId) if (externalId) { updatedProviderConfig = { ...updatedProviderConfig, externalId } @@ -2126,7 +2134,9 @@ export async function cleanupExternalWebhook( workflow: any, requestId: string ): Promise { - if (webhook.provider === 'airtable') { + if (webhook.provider === 'ashby') { + await deleteAshbyWebhook(webhook, requestId) + } else if (webhook.provider === 'airtable') { await deleteAirtableWebhook(webhook, workflow, requestId) } else if (webhook.provider === 'attio') { await deleteAttioWebhook(webhook, workflow, requestId) @@ -2148,3 +2158,160 @@ export async function cleanupExternalWebhook( await deleteLemlistWebhook(webhook, requestId) } } + +/** + * Creates a webhook subscription in Ashby via webhook.create API. + * Ashby uses Basic Auth and one webhook per event type (webhookType). + */ +export async function createAshbyWebhookSubscription( + webhookData: any, + requestId: string +): Promise<{ id: string } | undefined> { + try { + const { path, providerConfig } = webhookData + const { apiKey, triggerId } = providerConfig || {} + + if (!apiKey) { + throw new Error( + 'Ashby API Key is required. Please provide your API Key with apiKeysWrite permission in the trigger configuration.' + ) + } + + if (!triggerId) { + throw new Error('Trigger ID is required to create Ashby webhook.') + } + + const webhookTypeMap: Record = { + ashby_application_submit: 'applicationSubmit', + ashby_candidate_stage_change: 'candidateStageChange', + ashby_candidate_hire: 'candidateHire', + ashby_candidate_delete: 'candidateDelete', + ashby_job_create: 'jobCreate', + ashby_offer_create: 'offerCreate', + } + + const webhookType = webhookTypeMap[triggerId] + if (!webhookType) { + throw new Error(`Unknown Ashby triggerId: ${triggerId}. Add it to webhookTypeMap.`) + } + + const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` + const authString = Buffer.from(`${apiKey}:`).toString('base64') + + ashbyLogger.info(`[${requestId}] Creating Ashby webhook`, { + triggerId, + webhookType, + webhookId: webhookData.id, + }) + + const requestBody: Record = { + requestUrl: notificationUrl, + webhookType, + } + + const ashbyResponse = await fetch('https://api.ashbyhq.com/webhook.create', { + method: 'POST', + headers: { + Authorization: `Basic ${authString}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + const responseBody = await ashbyResponse.json().catch(() => ({})) + + if (!ashbyResponse.ok || !responseBody.success) { + const errorMessage = + responseBody.errorInfo?.message || responseBody.message || 'Unknown Ashby API error' + + let userFriendlyMessage = 'Failed to create webhook subscription in Ashby' + if (ashbyResponse.status === 401) { + userFriendlyMessage = + 'Invalid Ashby API Key. Please verify your API Key is correct and has apiKeysWrite permission.' + } else if (ashbyResponse.status === 403) { + userFriendlyMessage = + 'Access denied. Please ensure your Ashby API Key has the apiKeysWrite permission.' + } else if (errorMessage && errorMessage !== 'Unknown Ashby API error') { + userFriendlyMessage = `Ashby error: ${errorMessage}` + } + + throw new Error(userFriendlyMessage) + } + + const externalId = responseBody.results?.id + if (!externalId) { + throw new Error('Ashby webhook creation succeeded but no webhook ID was returned') + } + + ashbyLogger.info( + `[${requestId}] Successfully created Ashby webhook subscription ${externalId} for webhook ${webhookData.id}` + ) + return { id: externalId } + } catch (error: any) { + ashbyLogger.error( + `[${requestId}] Exception during Ashby webhook creation for webhook ${webhookData.id}.`, + { + message: error.message, + stack: error.stack, + } + ) + throw error + } +} + +/** + * Deletes an Ashby webhook subscription via webhook.delete API. + * Ashby uses POST with webhookId in the body (not DELETE method). + */ +export async function deleteAshbyWebhook(webhook: any, requestId: string): Promise { + try { + const config = getProviderConfig(webhook) + const apiKey = config.apiKey as string | undefined + const externalId = config.externalId as string | undefined + + if (!apiKey) { + ashbyLogger.warn( + `[${requestId}] Missing apiKey for Ashby webhook deletion ${webhook.id}, skipping cleanup` + ) + return + } + + if (!externalId) { + ashbyLogger.warn( + `[${requestId}] Missing externalId for Ashby webhook deletion ${webhook.id}, skipping cleanup` + ) + return + } + + const authString = Buffer.from(`${apiKey}:`).toString('base64') + + const ashbyResponse = await fetch('https://api.ashbyhq.com/webhook.delete', { + method: 'POST', + headers: { + Authorization: `Basic ${authString}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ webhookId: externalId }), + }) + + if (ashbyResponse.ok) { + await ashbyResponse.body?.cancel() + ashbyLogger.info( + `[${requestId}] Successfully deleted Ashby webhook subscription ${externalId}` + ) + } else if (ashbyResponse.status === 404) { + await ashbyResponse.body?.cancel() + ashbyLogger.info( + `[${requestId}] Ashby webhook ${externalId} not found during deletion (already removed)` + ) + } else { + const responseBody = await ashbyResponse.json().catch(() => ({})) + ashbyLogger.warn( + `[${requestId}] Failed to delete Ashby webhook (non-fatal): ${ashbyResponse.status}`, + { response: responseBody } + ) + } + } catch (error) { + ashbyLogger.warn(`[${requestId}] Error deleting Ashby webhook (non-fatal)`, error) + } +} diff --git a/apps/sim/lib/webhooks/utils.server.ts b/apps/sim/lib/webhooks/utils.server.ts index 76068e451f..6a1a00ddbf 100644 --- a/apps/sim/lib/webhooks/utils.server.ts +++ b/apps/sim/lib/webhooks/utils.server.ts @@ -1244,6 +1244,14 @@ export async function formatWebhookInput( return extractPageData(body) } + if (foundWebhook.provider === 'ashby') { + return { + ...(body.data || {}), + action: body.action, + data: body.data || {}, + } + } + if (foundWebhook.provider === 'stripe') { return body } diff --git a/apps/sim/triggers/ashby/application_submit.ts b/apps/sim/triggers/ashby/application_submit.ts new file mode 100644 index 0000000000..1c5500cbd3 --- /dev/null +++ b/apps/sim/triggers/ashby/application_submit.ts @@ -0,0 +1,41 @@ +import { AshbyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + ashbySetupInstructions, + ashbyTriggerOptions, + buildApplicationSubmitOutputs, + buildAshbyExtraFields, +} from '@/triggers/ashby/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Ashby Application Submitted Trigger + * + * This is the PRIMARY trigger - it includes the dropdown for selecting trigger type. + * Fires when a candidate submits an application or is manually added. + */ +export const ashbyApplicationSubmitTrigger: TriggerConfig = { + id: 'ashby_application_submit', + name: 'Ashby Application Submitted', + provider: 'ashby', + description: 'Trigger workflow when a new application is submitted', + version: '1.0.0', + icon: AshbyIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'ashby_application_submit', + triggerOptions: ashbyTriggerOptions, + includeDropdown: true, + setupInstructions: ashbySetupInstructions('Application Submitted'), + extraFields: buildAshbyExtraFields('ashby_application_submit'), + }), + + outputs: buildApplicationSubmitOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/ashby/candidate_delete.ts b/apps/sim/triggers/ashby/candidate_delete.ts new file mode 100644 index 0000000000..e70d26971b --- /dev/null +++ b/apps/sim/triggers/ashby/candidate_delete.ts @@ -0,0 +1,39 @@ +import { AshbyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + ashbySetupInstructions, + ashbyTriggerOptions, + buildAshbyExtraFields, + buildCandidateDeleteOutputs, +} from '@/triggers/ashby/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Ashby Candidate Deleted Trigger + * + * Fires when a candidate record is deleted from Ashby. + */ +export const ashbyCandidateDeleteTrigger: TriggerConfig = { + id: 'ashby_candidate_delete', + name: 'Ashby Candidate Deleted', + provider: 'ashby', + description: 'Trigger workflow when a candidate is deleted', + version: '1.0.0', + icon: AshbyIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'ashby_candidate_delete', + triggerOptions: ashbyTriggerOptions, + setupInstructions: ashbySetupInstructions('Candidate Deleted'), + extraFields: buildAshbyExtraFields('ashby_candidate_delete'), + }), + + outputs: buildCandidateDeleteOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/ashby/candidate_hire.ts b/apps/sim/triggers/ashby/candidate_hire.ts new file mode 100644 index 0000000000..529b15e7f2 --- /dev/null +++ b/apps/sim/triggers/ashby/candidate_hire.ts @@ -0,0 +1,40 @@ +import { AshbyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + ashbySetupInstructions, + ashbyTriggerOptions, + buildAshbyExtraFields, + buildCandidateHireOutputs, +} from '@/triggers/ashby/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Ashby Candidate Hired Trigger + * + * Fires when a candidate is hired. Also triggers applicationUpdate + * and candidateStageChange webhooks. + */ +export const ashbyCandidateHireTrigger: TriggerConfig = { + id: 'ashby_candidate_hire', + name: 'Ashby Candidate Hired', + provider: 'ashby', + description: 'Trigger workflow when a candidate is hired', + version: '1.0.0', + icon: AshbyIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'ashby_candidate_hire', + triggerOptions: ashbyTriggerOptions, + setupInstructions: ashbySetupInstructions('Candidate Hired'), + extraFields: buildAshbyExtraFields('ashby_candidate_hire'), + }), + + outputs: buildCandidateHireOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/ashby/candidate_stage_change.ts b/apps/sim/triggers/ashby/candidate_stage_change.ts new file mode 100644 index 0000000000..a1a43a6302 --- /dev/null +++ b/apps/sim/triggers/ashby/candidate_stage_change.ts @@ -0,0 +1,40 @@ +import { AshbyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + ashbySetupInstructions, + ashbyTriggerOptions, + buildAshbyExtraFields, + buildCandidateStageChangeOutputs, +} from '@/triggers/ashby/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Ashby Candidate Stage Change Trigger + * + * Fires when a candidate moves to a different interview stage. + * Also triggered by candidateHire events. + */ +export const ashbyCandidateStageChangeTrigger: TriggerConfig = { + id: 'ashby_candidate_stage_change', + name: 'Ashby Candidate Stage Change', + provider: 'ashby', + description: 'Trigger workflow when a candidate changes interview stages', + version: '1.0.0', + icon: AshbyIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'ashby_candidate_stage_change', + triggerOptions: ashbyTriggerOptions, + setupInstructions: ashbySetupInstructions('Candidate Stage Change'), + extraFields: buildAshbyExtraFields('ashby_candidate_stage_change'), + }), + + outputs: buildCandidateStageChangeOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/ashby/index.ts b/apps/sim/triggers/ashby/index.ts new file mode 100644 index 0000000000..8b2570569b --- /dev/null +++ b/apps/sim/triggers/ashby/index.ts @@ -0,0 +1,6 @@ +export { ashbyApplicationSubmitTrigger } from './application_submit' +export { ashbyCandidateDeleteTrigger } from './candidate_delete' +export { ashbyCandidateHireTrigger } from './candidate_hire' +export { ashbyCandidateStageChangeTrigger } from './candidate_stage_change' +export { ashbyJobCreateTrigger } from './job_create' +export { ashbyOfferCreateTrigger } from './offer_create' diff --git a/apps/sim/triggers/ashby/job_create.ts b/apps/sim/triggers/ashby/job_create.ts new file mode 100644 index 0000000000..88d60e13c2 --- /dev/null +++ b/apps/sim/triggers/ashby/job_create.ts @@ -0,0 +1,39 @@ +import { AshbyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + ashbySetupInstructions, + ashbyTriggerOptions, + buildAshbyExtraFields, + buildJobCreateOutputs, +} from '@/triggers/ashby/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Ashby Job Created Trigger + * + * Fires when a new job posting is created in Ashby. + */ +export const ashbyJobCreateTrigger: TriggerConfig = { + id: 'ashby_job_create', + name: 'Ashby Job Created', + provider: 'ashby', + description: 'Trigger workflow when a new job is created', + version: '1.0.0', + icon: AshbyIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'ashby_job_create', + triggerOptions: ashbyTriggerOptions, + setupInstructions: ashbySetupInstructions('Job Created'), + extraFields: buildAshbyExtraFields('ashby_job_create'), + }), + + outputs: buildJobCreateOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/ashby/offer_create.ts b/apps/sim/triggers/ashby/offer_create.ts new file mode 100644 index 0000000000..3b952b65b7 --- /dev/null +++ b/apps/sim/triggers/ashby/offer_create.ts @@ -0,0 +1,39 @@ +import { AshbyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + ashbySetupInstructions, + ashbyTriggerOptions, + buildAshbyExtraFields, + buildOfferCreateOutputs, +} from '@/triggers/ashby/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Ashby Offer Created Trigger + * + * Fires when a new offer is created for a candidate. + */ +export const ashbyOfferCreateTrigger: TriggerConfig = { + id: 'ashby_offer_create', + name: 'Ashby Offer Created', + provider: 'ashby', + description: 'Trigger workflow when a new offer is created', + version: '1.0.0', + icon: AshbyIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'ashby_offer_create', + triggerOptions: ashbyTriggerOptions, + setupInstructions: ashbySetupInstructions('Offer Created'), + extraFields: buildAshbyExtraFields('ashby_offer_create'), + }), + + outputs: buildOfferCreateOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/ashby/utils.ts b/apps/sim/triggers/ashby/utils.ts new file mode 100644 index 0000000000..ff25fcd5f8 --- /dev/null +++ b/apps/sim/triggers/ashby/utils.ts @@ -0,0 +1,237 @@ +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +/** + * Dropdown options for the Ashby trigger type selector. + */ +export const ashbyTriggerOptions = [ + { label: 'Application Submitted', id: 'ashby_application_submit' }, + { label: 'Candidate Stage Change', id: 'ashby_candidate_stage_change' }, + { label: 'Candidate Hired', id: 'ashby_candidate_hire' }, + { label: 'Candidate Deleted', id: 'ashby_candidate_delete' }, + { label: 'Job Created', id: 'ashby_job_create' }, + { label: 'Offer Created', id: 'ashby_offer_create' }, +] + +/** + * Generates setup instructions for Ashby webhooks. + * Webhooks are automatically created/deleted via the Ashby API. + */ +export function ashbySetupInstructions(eventType: string): string { + const instructions = [ + 'Enter your Ashby API Key above.', + 'You can find your API key in Ashby at Settings > API Keys. The key must have the apiKeysWrite permission.', + `Click "Save Configuration" to automatically create the webhook in Ashby for ${eventType} events.`, + 'The webhook will be automatically deleted when you remove this trigger.', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +/** + * Ashby-specific extra fields for triggers. + * Includes API key (required for automatic webhook creation). + */ +export function buildAshbyExtraFields(triggerId: string): SubBlockConfig[] { + return [ + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Ashby API key', + description: 'Required to create the webhook in Ashby. Must have apiKeysWrite permission.', + password: true, + required: true, + paramVisibility: 'user-only', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +/** + * Core fields present in all Ashby webhook payloads. + */ +const coreOutputs = { + action: { + type: 'string', + description: 'The webhook event type (e.g., applicationSubmit, candidateHire)', + }, +} as const + +/** + * Build outputs for applicationSubmit events. + * Payload: { action, data: { application: { id, createdAt, updatedAt, status, + * candidate: { id, name }, currentInterviewStage: { id, title }, + * job: { id, title } } } } + */ +export function buildApplicationSubmitOutputs(): Record { + return { + ...coreOutputs, + application: { + id: { type: 'string', description: 'Application UUID' }, + createdAt: { type: 'string', description: 'Application creation timestamp (ISO 8601)' }, + updatedAt: { + type: 'string', + description: 'Application last update timestamp (ISO 8601)', + }, + status: { + type: 'string', + description: 'Application status (Active, Hired, Archived, Lead)', + }, + candidate: { + id: { type: 'string', description: 'Candidate UUID' }, + name: { type: 'string', description: 'Candidate name' }, + }, + currentInterviewStage: { + id: { type: 'string', description: 'Current interview stage UUID' }, + title: { type: 'string', description: 'Current interview stage title' }, + }, + job: { + id: { type: 'string', description: 'Job UUID' }, + title: { type: 'string', description: 'Job title' }, + }, + }, + } as Record +} + +/** + * Build outputs for candidateStageChange events. + * Payload matches the application object structure (same as applicationUpdate). + * Payload: { action, data: { application: { id, createdAt, updatedAt, status, + * candidate: { id, name }, currentInterviewStage: { id, title, type }, + * job: { id, title } } } } + */ +export function buildCandidateStageChangeOutputs(): Record { + return { + ...coreOutputs, + application: { + id: { type: 'string', description: 'Application UUID' }, + createdAt: { type: 'string', description: 'Application creation timestamp (ISO 8601)' }, + updatedAt: { + type: 'string', + description: 'Application last update timestamp (ISO 8601)', + }, + status: { + type: 'string', + description: 'Application status (Active, Hired, Archived, Lead)', + }, + candidate: { + id: { type: 'string', description: 'Candidate UUID' }, + name: { type: 'string', description: 'Candidate name' }, + }, + currentInterviewStage: { + id: { type: 'string', description: 'Current interview stage UUID' }, + title: { type: 'string', description: 'Current interview stage title' }, + }, + job: { + id: { type: 'string', description: 'Job UUID' }, + title: { type: 'string', description: 'Job title' }, + }, + }, + } as Record +} + +/** + * Build outputs for candidateHire events. + * Payload: { action, data: { application: { id, createdAt, updatedAt, status, + * candidate: { id, name }, currentInterviewStage: { id, title }, + * job: { id, title } } } } + */ +export function buildCandidateHireOutputs(): Record { + return { + ...coreOutputs, + application: { + id: { type: 'string', description: 'Application UUID' }, + createdAt: { type: 'string', description: 'Application creation timestamp (ISO 8601)' }, + updatedAt: { + type: 'string', + description: 'Application last update timestamp (ISO 8601)', + }, + status: { type: 'string', description: 'Application status (Hired)' }, + candidate: { + id: { type: 'string', description: 'Candidate UUID' }, + name: { type: 'string', description: 'Candidate name' }, + }, + currentInterviewStage: { + id: { type: 'string', description: 'Current interview stage UUID' }, + title: { type: 'string', description: 'Current interview stage title' }, + }, + job: { + id: { type: 'string', description: 'Job UUID' }, + title: { type: 'string', description: 'Job title' }, + }, + }, + } as Record +} + +/** + * Build outputs for candidateDelete events. + * Payload: { action, data: { candidate: { id } } } + */ +export function buildCandidateDeleteOutputs(): Record { + return { + ...coreOutputs, + candidate: { + id: { type: 'string', description: 'Deleted candidate UUID' }, + }, + } as Record +} + +/** + * Build outputs for jobCreate events. + * Payload: { action, data: { job: { id, title, confidential, status, employmentType } } } + */ +export function buildJobCreateOutputs(): Record { + return { + ...coreOutputs, + job: { + id: { type: 'string', description: 'Job UUID' }, + title: { type: 'string', description: 'Job title' }, + confidential: { type: 'boolean', description: 'Whether the job is confidential' }, + status: { type: 'string', description: 'Job status (Open, Closed, Draft, Archived)' }, + employmentType: { + type: 'string', + description: 'Employment type (Full-time, Part-time, etc.)', + }, + }, + } as Record +} + +/** + * Build outputs for offerCreate events. + * Payload: { action, data: { offer: { id, decidedAt, applicationId, acceptanceStatus, + * offerStatus, latestVersion: { id } } } } + */ +export function buildOfferCreateOutputs(): Record { + return { + ...coreOutputs, + offer: { + id: { type: 'string', description: 'Offer UUID' }, + applicationId: { type: 'string', description: 'Associated application UUID' }, + acceptanceStatus: { + type: 'string', + description: + 'Offer acceptance status (Accepted, Declined, Pending, Created, Cancelled, WaitingOnResponse)', + }, + offerStatus: { + type: 'string', + description: + 'Offer process status (WaitingOnApprovalStart, WaitingOnOfferApproval, WaitingOnCandidateResponse, CandidateAccepted, CandidateRejected, OfferCancelled)', + }, + decidedAt: { + type: 'string', + description: + 'Offer decision timestamp (ISO 8601). Typically null at creation; populated after candidate responds.', + }, + latestVersion: { + id: { type: 'string', description: 'Latest offer version UUID' }, + }, + }, + } as Record +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 9ce20abaff..90171736d9 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -1,4 +1,12 @@ import { airtableWebhookTrigger } from '@/triggers/airtable' +import { + ashbyApplicationSubmitTrigger, + ashbyCandidateDeleteTrigger, + ashbyCandidateHireTrigger, + ashbyCandidateStageChangeTrigger, + ashbyJobCreateTrigger, + ashbyOfferCreateTrigger, +} from '@/triggers/ashby' import { attioCommentCreatedTrigger, attioCommentDeletedTrigger, @@ -166,6 +174,12 @@ import { whatsappWebhookTrigger } from '@/triggers/whatsapp' export const TRIGGER_REGISTRY: TriggerRegistry = { slack_webhook: slackWebhookTrigger, airtable_webhook: airtableWebhookTrigger, + ashby_application_submit: ashbyApplicationSubmitTrigger, + ashby_candidate_stage_change: ashbyCandidateStageChangeTrigger, + ashby_candidate_hire: ashbyCandidateHireTrigger, + ashby_candidate_delete: ashbyCandidateDeleteTrigger, + ashby_job_create: ashbyJobCreateTrigger, + ashby_offer_create: ashbyOfferCreateTrigger, attio_webhook: attioWebhookTrigger, attio_record_created: attioRecordCreatedTrigger, attio_record_updated: attioRecordUpdatedTrigger,