Skip to content

Commit 7515809

Browse files
aadamgoughaadamgough
andauthored
fix(grain): updated grain trigger to auto-establish trigger (#2666)
Co-authored-by: aadamgough <[email protected]>
1 parent 385e93f commit 7515809

File tree

9 files changed

+419
-87
lines changed

9 files changed

+419
-87
lines changed

apps/sim/app/api/webhooks/route.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,56 @@ export async function POST(request: NextRequest) {
581581
}
582582
// --- End RSS specific logic ---
583583

584+
if (savedWebhook && provider === 'grain') {
585+
logger.info(`[${requestId}] Grain provider detected. Creating Grain webhook subscription.`)
586+
try {
587+
const grainHookId = await createGrainWebhookSubscription(
588+
request,
589+
{
590+
id: savedWebhook.id,
591+
path: savedWebhook.path,
592+
providerConfig: savedWebhook.providerConfig,
593+
},
594+
requestId
595+
)
596+
597+
if (grainHookId) {
598+
// Update the webhook record with the external Grain hook ID
599+
const updatedConfig = {
600+
...(savedWebhook.providerConfig as Record<string, any>),
601+
externalId: grainHookId,
602+
}
603+
await db
604+
.update(webhook)
605+
.set({
606+
providerConfig: updatedConfig,
607+
updatedAt: new Date(),
608+
})
609+
.where(eq(webhook.id, savedWebhook.id))
610+
611+
savedWebhook.providerConfig = updatedConfig
612+
logger.info(`[${requestId}] Successfully created Grain webhook`, {
613+
grainHookId,
614+
webhookId: savedWebhook.id,
615+
})
616+
}
617+
} catch (err) {
618+
logger.error(
619+
`[${requestId}] Error creating Grain webhook subscription, rolling back webhook`,
620+
err
621+
)
622+
await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
623+
return NextResponse.json(
624+
{
625+
error: 'Failed to create webhook in Grain',
626+
details: err instanceof Error ? err.message : 'Unknown error',
627+
},
628+
{ status: 500 }
629+
)
630+
}
631+
}
632+
// --- End Grain specific logic ---
633+
584634
const status = targetWebhookId ? 200 : 201
585635
return NextResponse.json({ webhook: savedWebhook }, { status })
586636
} catch (error: any) {
@@ -947,3 +997,103 @@ async function createWebflowWebhookSubscription(
947997
throw error
948998
}
949999
}
1000+
1001+
// Helper function to create the webhook subscription in Grain
1002+
async function createGrainWebhookSubscription(
1003+
request: NextRequest,
1004+
webhookData: any,
1005+
requestId: string
1006+
): Promise<string | undefined> {
1007+
try {
1008+
const { path, providerConfig } = webhookData
1009+
const { apiKey, includeHighlights, includeParticipants, includeAiSummary } =
1010+
providerConfig || {}
1011+
1012+
if (!apiKey) {
1013+
logger.warn(`[${requestId}] Missing apiKey for Grain webhook creation.`, {
1014+
webhookId: webhookData.id,
1015+
})
1016+
throw new Error(
1017+
'Grain API Key is required. Please provide your Grain Personal Access Token in the trigger configuration.'
1018+
)
1019+
}
1020+
1021+
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
1022+
1023+
const grainApiUrl = 'https://api.grain.com/_/public-api/v2/hooks/create'
1024+
1025+
const requestBody: Record<string, any> = {
1026+
hook_url: notificationUrl,
1027+
}
1028+
1029+
// Build include object based on configuration
1030+
const include: Record<string, boolean> = {}
1031+
if (includeHighlights) {
1032+
include.highlights = true
1033+
}
1034+
if (includeParticipants) {
1035+
include.participants = true
1036+
}
1037+
if (includeAiSummary) {
1038+
include.ai_summary = true
1039+
}
1040+
if (Object.keys(include).length > 0) {
1041+
requestBody.include = include
1042+
}
1043+
1044+
const grainResponse = await fetch(grainApiUrl, {
1045+
method: 'POST',
1046+
headers: {
1047+
Authorization: `Bearer ${apiKey}`,
1048+
'Content-Type': 'application/json',
1049+
'Public-Api-Version': '2025-10-31',
1050+
},
1051+
body: JSON.stringify(requestBody),
1052+
})
1053+
1054+
const responseBody = await grainResponse.json()
1055+
1056+
if (!grainResponse.ok || responseBody.error) {
1057+
const errorMessage =
1058+
responseBody.error?.message ||
1059+
responseBody.error ||
1060+
responseBody.message ||
1061+
'Unknown Grain API error'
1062+
logger.error(
1063+
`[${requestId}] Failed to create webhook in Grain for webhook ${webhookData.id}. Status: ${grainResponse.status}`,
1064+
{ message: errorMessage, response: responseBody }
1065+
)
1066+
1067+
let userFriendlyMessage = 'Failed to create webhook subscription in Grain'
1068+
if (grainResponse.status === 401) {
1069+
userFriendlyMessage =
1070+
'Invalid Grain API Key. Please verify your Personal Access Token is correct.'
1071+
} else if (grainResponse.status === 403) {
1072+
userFriendlyMessage =
1073+
'Access denied. Please ensure your Grain API Key has appropriate permissions.'
1074+
} else if (errorMessage && errorMessage !== 'Unknown Grain API error') {
1075+
userFriendlyMessage = `Grain error: ${errorMessage}`
1076+
}
1077+
1078+
throw new Error(userFriendlyMessage)
1079+
}
1080+
1081+
logger.info(
1082+
`[${requestId}] Successfully created webhook in Grain for webhook ${webhookData.id}.`,
1083+
{
1084+
grainWebhookId: responseBody.id,
1085+
}
1086+
)
1087+
1088+
return responseBody.id
1089+
} catch (error: any) {
1090+
logger.error(
1091+
`[${requestId}] Exception during Grain webhook creation for webhook ${webhookData.id}.`,
1092+
{
1093+
message: error.message,
1094+
stack: error.stack,
1095+
}
1096+
)
1097+
throw error
1098+
}
1099+
}

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

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const telegramLogger = createLogger('TelegramWebhook')
88
const airtableLogger = createLogger('AirtableWebhook')
99
const typeformLogger = createLogger('TypeformWebhook')
1010
const calendlyLogger = createLogger('CalendlyWebhook')
11+
const grainLogger = createLogger('GrainWebhook')
1112

1213
function getProviderConfig(webhook: any): Record<string, any> {
1314
return (webhook.providerConfig as Record<string, any>) || {}
@@ -661,9 +662,58 @@ export async function deleteCalendlyWebhook(webhook: any, requestId: string): Pr
661662
}
662663
}
663664

665+
/**
666+
* Delete a Grain webhook
667+
* Don't fail webhook deletion if cleanup fails
668+
*/
669+
export async function deleteGrainWebhook(webhook: any, requestId: string): Promise<void> {
670+
try {
671+
const config = getProviderConfig(webhook)
672+
const apiKey = config.apiKey as string | undefined
673+
const externalId = config.externalId as string | undefined
674+
675+
if (!apiKey) {
676+
grainLogger.warn(
677+
`[${requestId}] Missing apiKey for Grain webhook deletion ${webhook.id}, skipping cleanup`
678+
)
679+
return
680+
}
681+
682+
if (!externalId) {
683+
grainLogger.warn(
684+
`[${requestId}] Missing externalId for Grain webhook deletion ${webhook.id}, skipping cleanup`
685+
)
686+
return
687+
}
688+
689+
const grainApiUrl = `https://api.grain.com/_/public-api/v2/hooks/${externalId}`
690+
691+
const grainResponse = await fetch(grainApiUrl, {
692+
method: 'DELETE',
693+
headers: {
694+
Authorization: `Bearer ${apiKey}`,
695+
'Content-Type': 'application/json',
696+
'Public-Api-Version': '2025-10-31',
697+
},
698+
})
699+
700+
if (!grainResponse.ok && grainResponse.status !== 404) {
701+
const responseBody = await grainResponse.json().catch(() => ({}))
702+
grainLogger.warn(
703+
`[${requestId}] Failed to delete Grain webhook (non-fatal): ${grainResponse.status}`,
704+
{ response: responseBody }
705+
)
706+
} else {
707+
grainLogger.info(`[${requestId}] Successfully deleted Grain webhook ${externalId}`)
708+
}
709+
} catch (error) {
710+
grainLogger.warn(`[${requestId}] Error deleting Grain webhook (non-fatal)`, error)
711+
}
712+
}
713+
664714
/**
665715
* Clean up external webhook subscriptions for a webhook
666-
* Handles Airtable, Teams, Telegram, Typeform, and Calendly cleanup
716+
* Handles Airtable, Teams, Telegram, Typeform, Calendly, and Grain cleanup
667717
* Don't fail deletion if cleanup fails
668718
*/
669719
export async function cleanupExternalWebhook(
@@ -681,5 +731,7 @@ export async function cleanupExternalWebhook(
681731
await deleteTypeformWebhook(webhook, requestId)
682732
} else if (webhook.provider === 'calendly') {
683733
await deleteCalendlyWebhook(webhook, requestId)
734+
} else if (webhook.provider === 'grain') {
735+
await deleteGrainWebhook(webhook, requestId)
684736
}
685737
}

apps/sim/triggers/grain/highlight_created.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,49 @@ export const grainHighlightCreatedTrigger: TriggerConfig = {
1212

1313
subBlocks: [
1414
{
15-
id: 'webhookUrlDisplay',
16-
title: 'Webhook URL',
15+
id: 'apiKey',
16+
title: 'API Key',
1717
type: 'short-input',
18-
readOnly: true,
19-
showCopyButton: true,
20-
useWebhookUrl: true,
21-
placeholder: 'Webhook URL will be generated',
18+
placeholder: 'Enter your Grain API key (Personal Access Token)',
19+
description: 'Required to create the webhook in Grain.',
20+
password: true,
21+
required: true,
2222
mode: 'trigger',
2323
condition: {
2424
field: 'selectedTriggerId',
2525
value: 'grain_highlight_created',
2626
},
2727
},
2828
{
29-
id: 'webhookSecret',
30-
title: 'Webhook Secret',
31-
type: 'short-input',
32-
placeholder: 'Enter a strong secret',
33-
description: 'Validates that webhook deliveries originate from Grain.',
34-
password: true,
35-
required: false,
29+
id: 'includeHighlights',
30+
title: 'Include Highlights',
31+
type: 'switch',
32+
description: 'Include highlights/clips in webhook payload.',
33+
defaultValue: false,
34+
mode: 'trigger',
35+
condition: {
36+
field: 'selectedTriggerId',
37+
value: 'grain_highlight_created',
38+
},
39+
},
40+
{
41+
id: 'includeParticipants',
42+
title: 'Include Participants',
43+
type: 'switch',
44+
description: 'Include participant list in webhook payload.',
45+
defaultValue: false,
46+
mode: 'trigger',
47+
condition: {
48+
field: 'selectedTriggerId',
49+
value: 'grain_highlight_created',
50+
},
51+
},
52+
{
53+
id: 'includeAiSummary',
54+
title: 'Include AI Summary',
55+
type: 'switch',
56+
description: 'Include AI-generated summary in webhook payload.',
57+
defaultValue: false,
3658
mode: 'trigger',
3759
condition: {
3860
field: 'selectedTriggerId',

apps/sim/triggers/grain/highlight_updated.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,49 @@ export const grainHighlightUpdatedTrigger: TriggerConfig = {
1212

1313
subBlocks: [
1414
{
15-
id: 'webhookUrlDisplay',
16-
title: 'Webhook URL',
15+
id: 'apiKey',
16+
title: 'API Key',
1717
type: 'short-input',
18-
readOnly: true,
19-
showCopyButton: true,
20-
useWebhookUrl: true,
21-
placeholder: 'Webhook URL will be generated',
18+
placeholder: 'Enter your Grain API key (Personal Access Token)',
19+
description: 'Required to create the webhook in Grain.',
20+
password: true,
21+
required: true,
2222
mode: 'trigger',
2323
condition: {
2424
field: 'selectedTriggerId',
2525
value: 'grain_highlight_updated',
2626
},
2727
},
2828
{
29-
id: 'webhookSecret',
30-
title: 'Webhook Secret',
31-
type: 'short-input',
32-
placeholder: 'Enter a strong secret',
33-
description: 'Validates that webhook deliveries originate from Grain.',
34-
password: true,
35-
required: false,
29+
id: 'includeHighlights',
30+
title: 'Include Highlights',
31+
type: 'switch',
32+
description: 'Include highlights/clips in webhook payload.',
33+
defaultValue: false,
34+
mode: 'trigger',
35+
condition: {
36+
field: 'selectedTriggerId',
37+
value: 'grain_highlight_updated',
38+
},
39+
},
40+
{
41+
id: 'includeParticipants',
42+
title: 'Include Participants',
43+
type: 'switch',
44+
description: 'Include participant list in webhook payload.',
45+
defaultValue: false,
46+
mode: 'trigger',
47+
condition: {
48+
field: 'selectedTriggerId',
49+
value: 'grain_highlight_updated',
50+
},
51+
},
52+
{
53+
id: 'includeAiSummary',
54+
title: 'Include AI Summary',
55+
type: 'switch',
56+
description: 'Include AI-generated summary in webhook payload.',
57+
defaultValue: false,
3658
mode: 'trigger',
3759
condition: {
3860
field: 'selectedTriggerId',

0 commit comments

Comments
 (0)