Skip to content
2 changes: 2 additions & 0 deletions apps/docs/content/docs/en/tools/ashby.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 */}

Expand Down
15 changes: 15 additions & 0 deletions apps/docs/content/docs/en/tools/evernote.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions apps/docs/content/docs/en/tools/fathom.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions apps/docs/content/docs/en/tools/obsidian.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 21 additions & 0 deletions apps/sim/blocks/blocks/ashby.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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: {
Expand Down
171 changes: 169 additions & 2 deletions apps/sim/lib/webhooks/provider-subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -1974,6 +1975,7 @@ type RecreateCheckInput = {
/** Providers that create external webhook subscriptions */
const PROVIDERS_WITH_EXTERNAL_SUBSCRIPTIONS = new Set([
'airtable',
'ashby',
'attio',
'calendly',
'fathom',
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -2126,7 +2134,9 @@ export async function cleanupExternalWebhook(
workflow: any,
requestId: string
): Promise<void> {
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)
Expand All @@ -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<string, string> = {
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<string, unknown> = {
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),
Comment on lines +2207 to +2218
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No webhook payload origin verification

webhook.create is called with only requestUrl and webhookType. Ashby's API also accepts a secretToken parameter: when set, Ashby signs every delivery with HMAC-SHA256 and includes the signature in the X-Ashby-Signature header. Without it, any actor who discovers the webhook URL (which is a UUID path but still guessable or leak-able) can POST an arbitrary payload and trigger workflows.

Other providers in this codebase — for example, Calendly — generate and persist a webhookSecret so incoming requests can be verified against the signature header before the payload is acted upon. Consider adopting the same approach here: generate a random token at creation time, include it in requestBody, store it alongside externalId in providerConfig, and verify the X-Ashby-Signature header in formatWebhookInput or the processor before accepting the payload.

})

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<void> {
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)
}
}
8 changes: 8 additions & 0 deletions apps/sim/lib/webhooks/utils.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
41 changes: 41 additions & 0 deletions apps/sim/triggers/ashby/application_submit.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
}
Loading
Loading