Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 55 additions & 9 deletions apps/sim/app/api/webhooks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ export async function POST(request: NextRequest) {
if (savedWebhook && provider === 'grain') {
logger.info(`[${requestId}] Grain provider detected. Creating Grain webhook subscription.`)
try {
const grainHookId = await createGrainWebhookSubscription(
const grainResult = await createGrainWebhookSubscription(
request,
{
id: savedWebhook.id,
Expand All @@ -754,11 +754,12 @@ export async function POST(request: NextRequest) {
requestId
)

if (grainHookId) {
// Update the webhook record with the external Grain hook ID
if (grainResult) {
// Update the webhook record with the external Grain hook ID and event types for filtering
const updatedConfig = {
...(savedWebhook.providerConfig as Record<string, any>),
externalId: grainHookId,
externalId: grainResult.id,
eventTypes: grainResult.eventTypes,
}
await db
.update(webhook)
Expand All @@ -770,7 +771,8 @@ export async function POST(request: NextRequest) {

savedWebhook.providerConfig = updatedConfig
logger.info(`[${requestId}] Successfully created Grain webhook`, {
grainHookId,
grainHookId: grainResult.id,
eventTypes: grainResult.eventTypes,
webhookId: savedWebhook.id,
})
}
Expand Down Expand Up @@ -1176,10 +1178,10 @@ async function createGrainWebhookSubscription(
request: NextRequest,
webhookData: any,
requestId: string
): Promise<string | undefined> {
): Promise<{ id: string; eventTypes: string[] } | undefined> {
try {
const { path, providerConfig } = webhookData
const { apiKey, includeHighlights, includeParticipants, includeAiSummary } =
const { apiKey, triggerId, includeHighlights, includeParticipants, includeAiSummary } =
providerConfig || {}

if (!apiKey) {
Expand All @@ -1191,12 +1193,53 @@ async function createGrainWebhookSubscription(
)
}

// Map trigger IDs to Grain API hook_type (only 2 options: recording_added, upload_status)
const hookTypeMap: Record<string, string> = {
grain_webhook: 'recording_added',
grain_recording_created: 'recording_added',
grain_recording_updated: 'recording_added',
grain_highlight_created: 'recording_added',
grain_highlight_updated: 'recording_added',
grain_story_created: 'recording_added',
grain_upload_status: 'upload_status',
}

const eventTypeMap: Record<string, string[]> = {
grain_webhook: [],
grain_recording_created: ['recording_added'],
grain_recording_updated: ['recording_updated'],
grain_highlight_created: ['highlight_created'],
grain_highlight_updated: ['highlight_updated'],
grain_story_created: ['story_created'],
grain_upload_status: ['upload_status'],
}

const hookType = hookTypeMap[triggerId] ?? 'recording_added'
const eventTypes = eventTypeMap[triggerId] ?? []

if (!hookTypeMap[triggerId]) {
logger.warn(
`[${requestId}] Unknown triggerId for Grain: ${triggerId}, defaulting to recording_added`,
{
webhookId: webhookData.id,
}
)
}

logger.info(`[${requestId}] Creating Grain webhook`, {
triggerId,
hookType,
eventTypes,
webhookId: webhookData.id,
})

const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`

const grainApiUrl = 'https://api.grain.com/_/public-api/v2/hooks/create'

const requestBody: Record<string, any> = {
hook_url: notificationUrl,
hook_type: hookType,
}

// Build include object based on configuration
Expand Down Expand Up @@ -1226,8 +1269,10 @@ async function createGrainWebhookSubscription(

const responseBody = await grainResponse.json()

if (!grainResponse.ok || responseBody.error) {
if (!grainResponse.ok || responseBody.error || responseBody.errors) {
logger.warn('[App] Grain response body:', responseBody)
const errorMessage =
responseBody.errors?.detail ||
responseBody.error?.message ||
responseBody.error ||
responseBody.message ||
Expand Down Expand Up @@ -1255,10 +1300,11 @@ async function createGrainWebhookSubscription(
`[${requestId}] Successfully created webhook in Grain for webhook ${webhookData.id}.`,
{
grainWebhookId: responseBody.id,
eventTypes,
}
)

return responseBody.id
return { id: responseBody.id, eventTypes }
} catch (error: any) {
logger.error(
`[${requestId}] Exception during Grain webhook creation for webhook ${webhookData.id}.`,
Expand Down
13 changes: 13 additions & 0 deletions apps/sim/lib/webhooks/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,19 @@ export function shouldSkipWebhookEvent(webhook: any, body: any, requestId: strin
}
}

if (webhook.provider === 'grain') {
const eventTypes = providerConfig.eventTypes
if (eventTypes && Array.isArray(eventTypes) && eventTypes.length > 0) {
const eventType = body?.type
if (eventType && !eventTypes.includes(eventType)) {
logger.info(
`[${requestId}] Grain event type '${eventType}' not in allowed list for webhook ${webhook.id}, skipping`
)
return true
}
}
}

return false
}

Expand Down
11 changes: 11 additions & 0 deletions apps/sim/tools/grain/create_hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export const grainCreateHookTool: ToolConfig<GrainCreateHookParams, GrainCreateH
visibility: 'user-or-llm',
description: 'Webhook endpoint URL (must respond 2xx)',
},
hookType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Type of webhook: "recording_added" or "upload_status"',
},
filterBeforeDatetime: {
type: 'string',
required: false,
Expand Down Expand Up @@ -81,6 +87,7 @@ export const grainCreateHookTool: ToolConfig<GrainCreateHookParams, GrainCreateH
body: (params) => {
const body: Record<string, any> = {
hook_url: params.hookUrl,
hook_type: params.hookType,
}

const filter: Record<string, any> = {}
Expand Down Expand Up @@ -147,6 +154,10 @@ export const grainCreateHookTool: ToolConfig<GrainCreateHookParams, GrainCreateH
type: 'string',
description: 'The webhook URL',
},
hook_type: {
type: 'string',
description: 'Type of hook: recording_added or upload_status',
},
filter: {
type: 'object',
description: 'Applied filters',
Expand Down
1 change: 1 addition & 0 deletions apps/sim/tools/grain/list_hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const grainListHooksTool: ToolConfig<GrainListHooksParams, GrainListHooks
id: { type: 'string', description: 'Hook UUID' },
enabled: { type: 'boolean', description: 'Whether hook is active' },
hook_url: { type: 'string', description: 'Webhook URL' },
hook_type: { type: 'string', description: 'Type: recording_added or upload_status' },
filter: { type: 'object', description: 'Applied filters' },
include: { type: 'object', description: 'Included fields' },
inserted_at: { type: 'string', description: 'Creation timestamp' },
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/tools/grain/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export interface GrainHook {
id: string
enabled: boolean
hook_url: string
hook_type: 'recording_added' | 'upload_status'
filter: GrainRecordingFilter
include: GrainRecordingInclude
inserted_at: string
Expand Down Expand Up @@ -192,6 +193,7 @@ export interface GrainListMeetingTypesResponse extends ToolResponse {
export interface GrainCreateHookParams {
apiKey: string
hookUrl: string
hookType: 'recording_added' | 'upload_status'
filterBeforeDatetime?: string
filterAfterDatetime?: string
filterParticipantScope?: 'internal' | 'external'
Expand Down