Skip to content

Commit f6b802e

Browse files
authored
fix(sendgrid): fix active field coercion, add pagination, tighten output typing (#5368)
* fix(sendgrid): fix active field coercion, add pagination, tighten output typing - Fix active field for create_template_version being sent as the string "true"/"false" instead of the SendGrid-required int 0/1 - Add missing authMode: ApiKey on SendGridBlock - Add pageToken/nextPageToken pagination support to list_templates and list_all_lists (SendGrid page_token cursor, parsed from _metadata.next) - Fix nullable output fields to use ?? null / ?? [] with optional: true across get_contact, search_contacts, remove_contacts_from_list, create_template_version, add_contact, send_mail - Remove dead data.templates fallback in list_templates (API only ever returns result) - Remove unused UpdateContactParams/UpdateListParams/UpdateTemplateParams dead types; make CreateTemplateParams.generation optional to match actual tool behavior * fix(sendgrid): address Cursor Bugbot findings on pagination and active coercion - Gate listPageToken/templatePageToken remap on operation so a stale token from the other list operation can't override the intended one - Fix active coercion to also treat a real boolean true (from a dynamic <Block.output> reference) as active, not just the dropdown string 'true' * fix(sendgrid): coerce active to int at the tool layer too Per Greptile: the block-level active coercion only covered the UI path. A direct sendgrid_create_template_version tool invocation with a boolean active would still send a raw boolean to SendGrid. Coerce to 0/1 in the tool's own request body so both paths are correct. * fix(sendgrid): always send page_size on list_templates SendGrid's GET /v3/templates requires page_size on every request (no server-side default) — omitting it errors. Default to 20 to match our own documented default when the caller doesn't set one. * fix(sendgrid): explicit false/'false' check for active flag Per Cursor Bugbot: params.active ? 1 : 0 treated any truthy string (including "false") as active. Extracted a toActiveFlag helper that only treats real false or the string 'false' as inactive, everything else (including unset) defaults to active — matches the tool's documented default. * fix(sendgrid): handle numeric 0 in toActiveFlag Per Greptile: the block coerces active to a number (0/1) before calling the tool, but toActiveFlag only checked for false/'false', so the block's inactive selection (0) fell through to the "active" branch. Check against an explicit inactive-values set covering the boolean, string, and numeric forms. * fix(sendgrid): nest add_contact custom fields under custom_fields Pre-existing bug (predates this PR): custom fields were merged onto the contact object as top-level sibling keys via safeAssign/Object.assign, but SendGrid's PUT /v3/marketing/contacts requires them nested under a custom_fields object. SendGrid silently drops unrecognized top-level keys, so the documented customFields param never actually reached SendGrid. Caught during a final adversarial re-verification pass before merge. * fix(sendgrid): document consistent page_size requirement for list_templates pagination Per Cursor Bugbot: list_templates always defaults page_size to 20 when unset (required by SendGrid), so a follow-up pageToken-only call after a first call with a larger pageSize would silently shrink to 20 and desync page boundaries. This is inherent to a stateless tool call (SendGrid requires page_size on every request, and the tool has no way to remember the prior call's value), so clarify via param description and UI placeholder that callers must repeat the same pageSize across paginated calls. * chore(api-validation): bump stale route-count ratchet baseline 883->884 Unrelated to the SendGrid work in this branch. staging's own HEAD already has 884 compliant Zod-backed API routes (0 non-Zod), but this ratchet baseline was never bumped when that route landed, so any PR rebasing onto current staging fails check:api-validation:strict with "route count increased from 883 to 884". All routes remain fully Zod-backed; this is a mechanical counter update, not a policy change. * fix(sendgrid): dedupe active coercion between block and tool Per Cursor Bugbot: the block's pre-coercion only recognized the dropdown string 'true' or boolean true as active, so a dynamic reference producing numeric 1 or string '1' fell through to 0 and silently created an inactive template version. Exported the tool's toActiveFlag and reused it in the block instead of duplicating the inactive-value logic, so both layers can no longer drift out of sync.
1 parent 11c7a36 commit f6b802e

10 files changed

Lines changed: 151 additions & 61 deletions

File tree

apps/sim/blocks/blocks/sendgrid.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { SendgridIcon } from '@/components/icons'
22
import type { BlockConfig, BlockMeta } from '@/blocks/types'
3-
import { IntegrationType } from '@/blocks/types'
3+
import { AuthMode, IntegrationType } from '@/blocks/types'
44
import { normalizeFileInput } from '@/blocks/utils'
5+
import { toActiveFlag } from '@/tools/sendgrid/create_template_version'
56
import type { SendMailResult } from '@/tools/sendgrid/types'
67

78
export const SendGridBlock: BlockConfig<SendMailResult> = {
@@ -13,6 +14,7 @@ export const SendGridBlock: BlockConfig<SendMailResult> = {
1314
docsLink: 'https://docs.sim.ai/integrations/sendgrid',
1415
category: 'tools',
1516
integrationType: IntegrationType.Email,
17+
authMode: AuthMode.ApiKey,
1618
bgColor: '#1A82E2',
1719
icon: SendgridIcon,
1820

@@ -387,6 +389,14 @@ Return ONLY the JSON array.`,
387389
condition: { field: 'operation', value: 'list_all_lists' },
388390
mode: 'advanced',
389391
},
392+
{
393+
id: 'listPageToken',
394+
title: 'Page Token',
395+
type: 'short-input',
396+
placeholder: 'Page token from a previous response',
397+
condition: { field: 'operation', value: 'list_all_lists' },
398+
mode: 'advanced',
399+
},
390400
// Template fields
391401
{
392402
id: 'templateName',
@@ -434,6 +444,14 @@ Return ONLY the JSON array.`,
434444
condition: { field: 'operation', value: 'list_templates' },
435445
mode: 'advanced',
436446
},
447+
{
448+
id: 'templatePageToken',
449+
title: 'Page Token',
450+
type: 'short-input',
451+
placeholder: 'Page token from a previous response (keep Page Size the same)',
452+
condition: { field: 'operation', value: 'list_templates' },
453+
mode: 'advanced',
454+
},
437455
{
438456
id: 'versionName',
439457
title: 'Version Name',
@@ -579,7 +597,10 @@ Return ONLY the HTML content.`,
579597
templateGenerations,
580598
listPageSize,
581599
templatePageSize,
600+
listPageToken,
601+
templatePageToken,
582602
attachments,
603+
active,
583604
...rest
584605
} = params
585606

@@ -599,7 +620,11 @@ Return ONLY the HTML content.`,
599620
...(templateGenerations && { generations: templateGenerations }),
600621
...(listPageSize && { pageSize: listPageSize }),
601622
...(templatePageSize && { pageSize: templatePageSize }),
623+
...(operation === 'list_all_lists' && listPageToken && { pageToken: listPageToken }),
624+
...(operation === 'list_templates' &&
625+
templatePageToken && { pageToken: templatePageToken }),
602626
...(normalizedAttachments && { attachments: normalizedAttachments }),
627+
...(active !== undefined && { active: toActiveFlag(active) }),
603628
}
604629
},
605630
},
@@ -637,12 +662,14 @@ Return ONLY the HTML content.`,
637662
listName: { type: 'string', description: 'List name' },
638663
listId: { type: 'string', description: 'List ID' },
639664
listPageSize: { type: 'number', description: 'Page size for listing lists' },
665+
listPageToken: { type: 'string', description: 'Page token for listing lists' },
640666
// Template inputs
641667
templateName: { type: 'string', description: 'Template name' },
642668
templateId: { type: 'string', description: 'Template ID' },
643669
generation: { type: 'string', description: 'Template generation' },
644670
templateGenerations: { type: 'string', description: 'Filter templates by generation' },
645671
templatePageSize: { type: 'number', description: 'Page size for listing templates' },
672+
templatePageToken: { type: 'string', description: 'Page token for listing templates' },
646673
versionName: { type: 'string', description: 'Template version name' },
647674
templateSubject: { type: 'string', description: 'Template subject' },
648675
htmlContent: { type: 'string', description: 'HTML content' },
@@ -677,6 +704,10 @@ Return ONLY the HTML content.`,
677704
templates: { type: 'json', description: 'Array of templates' },
678705
generation: { type: 'string', description: 'Template generation' },
679706
versions: { type: 'json', description: 'Array of template versions' },
707+
nextPageToken: {
708+
type: 'string',
709+
description: 'Token for the next page of results (list_all_lists, list_templates)',
710+
},
680711
// Template version outputs
681712
templateId: { type: 'string', description: 'Template ID' },
682713
active: { type: 'boolean', description: 'Whether template version is active' },

apps/sim/tools/sendgrid/add_contact.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { safeAssign } from '@/tools/safe-assign'
21
import type {
32
AddContactParams,
43
ContactResult,
@@ -73,7 +72,7 @@ export const sendGridAddContactTool: ToolConfig<AddContactParams, ContactResult>
7372
typeof params.customFields === 'string'
7473
? JSON.parse(params.customFields)
7574
: params.customFields
76-
safeAssign(contact, customFields as Record<string, unknown>)
75+
contact.custom_fields = customFields as Record<string, unknown>
7776
}
7877

7978
const body: SendGridContactRequest = {
@@ -99,7 +98,7 @@ export const sendGridAddContactTool: ToolConfig<AddContactParams, ContactResult>
9998
return {
10099
success: true,
101100
output: {
102-
jobId: data.job_id,
101+
jobId: data.job_id ?? null,
103102
email: params?.email || '',
104103
firstName: params?.firstName,
105104
lastName: params?.lastName,
@@ -110,10 +109,14 @@ export const sendGridAddContactTool: ToolConfig<AddContactParams, ContactResult>
110109
},
111110

112111
outputs: {
113-
jobId: { type: 'string', description: 'Job ID for tracking the async contact creation' },
112+
jobId: {
113+
type: 'string',
114+
description: 'Job ID for tracking the async contact creation',
115+
optional: true,
116+
},
114117
email: { type: 'string', description: 'Contact email address' },
115-
firstName: { type: 'string', description: 'Contact first name' },
116-
lastName: { type: 'string', description: 'Contact last name' },
118+
firstName: { type: 'string', description: 'Contact first name', optional: true },
119+
lastName: { type: 'string', description: 'Contact last name', optional: true },
117120
message: { type: 'string', description: 'Status message' },
118121
},
119122
}

apps/sim/tools/sendgrid/create_template_version.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import type {
55
} from '@/tools/sendgrid/types'
66
import type { ToolConfig } from '@/tools/types'
77

8+
const INACTIVE_VALUES: unknown[] = [false, 'false', 0, '0']
9+
10+
/** Coerces any dynamic-reference form of SendGrid's active flag (boolean, string, or
11+
* number) to the 0/1 integer the API requires. Shared with the block's own
12+
* pre-coercion in blocks/blocks/sendgrid.ts so both layers stay in sync. */
13+
export function toActiveFlag(active: unknown): 0 | 1 {
14+
if (active === undefined) return 1
15+
return INACTIVE_VALUES.includes(active) ? 0 : 1
16+
}
17+
818
export const sendGridCreateTemplateVersionTool: ToolConfig<
919
CreateTemplateVersionParams,
1020
TemplateVersionResult
@@ -70,7 +80,7 @@ export const sendGridCreateTemplateVersionTool: ToolConfig<
7080
const body: SendGridTemplateVersionRequest = {
7181
name: params.name,
7282
subject: params.subject,
73-
active: params.active !== undefined ? params.active : 1,
83+
active: toActiveFlag(params.active),
7484
}
7585

7686
if (params.htmlContent) {
@@ -101,9 +111,9 @@ export const sendGridCreateTemplateVersionTool: ToolConfig<
101111
name: data.name,
102112
subject: data.subject,
103113
active: data.active === 1,
104-
htmlContent: data.html_content,
105-
plainContent: data.plain_content,
106-
updatedAt: data.updated_at,
114+
htmlContent: data.html_content ?? null,
115+
plainContent: data.plain_content ?? null,
116+
updatedAt: data.updated_at ?? null,
107117
},
108118
}
109119
},
@@ -114,8 +124,8 @@ export const sendGridCreateTemplateVersionTool: ToolConfig<
114124
name: { type: 'string', description: 'Version name' },
115125
subject: { type: 'string', description: 'Email subject' },
116126
active: { type: 'boolean', description: 'Whether this version is active' },
117-
htmlContent: { type: 'string', description: 'HTML content' },
118-
plainContent: { type: 'string', description: 'Plain text content' },
119-
updatedAt: { type: 'string', description: 'Last update timestamp' },
127+
htmlContent: { type: 'string', description: 'HTML content', optional: true },
128+
plainContent: { type: 'string', description: 'Plain text content', optional: true },
129+
updatedAt: { type: 'string', description: 'Last update timestamp', optional: true },
120130
},
121131
}

apps/sim/tools/sendgrid/get_contact.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,24 @@ export const sendGridGetContactTool: ToolConfig<GetContactParams, ContactResult>
4747
lastName: data.last_name,
4848
createdAt: data.created_at,
4949
updatedAt: data.updated_at,
50-
listIds: data.list_ids,
51-
customFields: data.custom_fields,
50+
listIds: data.list_ids ?? [],
51+
customFields: data.custom_fields ?? null,
5252
},
5353
}
5454
},
5555

5656
outputs: {
5757
id: { type: 'string', description: 'Contact ID' },
5858
email: { type: 'string', description: 'Contact email address' },
59-
firstName: { type: 'string', description: 'Contact first name' },
60-
lastName: { type: 'string', description: 'Contact last name' },
61-
createdAt: { type: 'string', description: 'Creation timestamp' },
62-
updatedAt: { type: 'string', description: 'Last update timestamp' },
63-
listIds: { type: 'json', description: 'Array of list IDs the contact belongs to' },
64-
customFields: { type: 'json', description: 'Custom field values' },
59+
firstName: { type: 'string', description: 'Contact first name', optional: true },
60+
lastName: { type: 'string', description: 'Contact last name', optional: true },
61+
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
62+
updatedAt: { type: 'string', description: 'Last update timestamp', optional: true },
63+
listIds: {
64+
type: 'json',
65+
description: 'Array of list IDs the contact belongs to',
66+
optional: true,
67+
},
68+
customFields: { type: 'json', description: 'Custom field values', optional: true },
6569
},
6670
}

apps/sim/tools/sendgrid/list_all_lists.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ export const sendGridListAllListsTool: ToolConfig<ListAllListsParams, ListsResul
1818
type: 'number',
1919
required: false,
2020
visibility: 'user-or-llm',
21-
description: 'Number of lists to return per page (default: 100)',
21+
description: 'Number of lists to return per page (default: 100, max: 1000)',
22+
},
23+
pageToken: {
24+
type: 'string',
25+
required: false,
26+
visibility: 'user-or-llm',
27+
description: 'Page token from a previous response (nextPageToken) to fetch the next page',
2228
},
2329
},
2430

@@ -28,6 +34,9 @@ export const sendGridListAllListsTool: ToolConfig<ListAllListsParams, ListsResul
2834
if (params.pageSize) {
2935
url.searchParams.append('page_size', params.pageSize.toString())
3036
}
37+
if (params.pageToken) {
38+
url.searchParams.append('page_token', params.pageToken)
39+
}
3140
return url.toString()
3241
},
3342
method: 'GET',
@@ -42,17 +51,35 @@ export const sendGridListAllListsTool: ToolConfig<ListAllListsParams, ListsResul
4251
throw new Error(error.errors?.[0]?.message || 'Failed to list all lists')
4352
}
4453

45-
const data = (await response.json()) as { result?: SendGridList[] }
54+
const data = (await response.json()) as {
55+
result?: SendGridList[]
56+
_metadata?: { next?: string }
57+
}
58+
59+
let nextPageToken: string | null = null
60+
if (data._metadata?.next) {
61+
try {
62+
nextPageToken = new URL(data._metadata.next).searchParams.get('page_token')
63+
} catch {
64+
nextPageToken = null
65+
}
66+
}
4667

4768
return {
4869
success: true,
4970
output: {
5071
lists: data.result || [],
72+
nextPageToken,
5173
},
5274
}
5375
},
5476

5577
outputs: {
5678
lists: { type: 'json', description: 'Array of lists' },
79+
nextPageToken: {
80+
type: 'string',
81+
description: 'Token to pass as pageToken to fetch the next page, if more results exist',
82+
optional: true,
83+
},
5784
},
5885
}

apps/sim/tools/sendgrid/list_templates.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,16 @@ export const sendGridListTemplatesTool: ToolConfig<ListTemplatesParams, Template
2424
type: 'number',
2525
required: false,
2626
visibility: 'user-or-llm',
27-
description: 'Number of templates to return per page (default: 20)',
27+
description:
28+
'Number of templates to return per page (default: 20, max: 200). ' +
29+
'When paginating with pageToken, pass the same pageSize used on the first request ' +
30+
'to keep page boundaries consistent.',
31+
},
32+
pageToken: {
33+
type: 'string',
34+
required: false,
35+
visibility: 'user-or-llm',
36+
description: 'Page token from a previous response (nextPageToken) to fetch the next page',
2837
},
2938
},
3039

@@ -34,8 +43,9 @@ export const sendGridListTemplatesTool: ToolConfig<ListTemplatesParams, Template
3443
if (params.generations) {
3544
url.searchParams.append('generations', params.generations)
3645
}
37-
if (params.pageSize) {
38-
url.searchParams.append('page_size', params.pageSize.toString())
46+
url.searchParams.append('page_size', (params.pageSize || 20).toString())
47+
if (params.pageToken) {
48+
url.searchParams.append('page_token', params.pageToken)
3949
}
4050
return url.toString()
4151
},
@@ -53,18 +63,33 @@ export const sendGridListTemplatesTool: ToolConfig<ListTemplatesParams, Template
5363

5464
const data = (await response.json()) as {
5565
result?: SendGridTemplate[]
56-
templates?: SendGridTemplate[]
66+
_metadata?: { next?: string }
67+
}
68+
69+
let nextPageToken: string | null = null
70+
if (data._metadata?.next) {
71+
try {
72+
nextPageToken = new URL(data._metadata.next).searchParams.get('page_token')
73+
} catch {
74+
nextPageToken = null
75+
}
5776
}
5877

5978
return {
6079
success: true,
6180
output: {
62-
templates: data.result || data.templates || [],
81+
templates: data.result || [],
82+
nextPageToken,
6383
},
6484
}
6585
},
6686

6787
outputs: {
6888
templates: { type: 'json', description: 'Array of templates' },
89+
nextPageToken: {
90+
type: 'string',
91+
description: 'Token to pass as pageToken to fetch the next page, if more results exist',
92+
optional: true,
93+
},
6994
},
7095
}

apps/sim/tools/sendgrid/remove_contacts_from_list.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ export const sendGridRemoveContactsFromListTool: ToolConfig<
5656
return {
5757
success: true,
5858
output: {
59-
jobId: data.job_id,
59+
jobId: data.job_id ?? null,
6060
},
6161
}
6262
},
6363

6464
outputs: {
65-
jobId: { type: 'string', description: 'Job ID for the request' },
65+
jobId: { type: 'string', description: 'Job ID for the request', optional: true },
6666
},
6767
}

apps/sim/tools/sendgrid/search_contacts.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,17 @@ export const sendGridSearchContactsTool: ToolConfig<SearchContactsParams, Contac
5454
success: true,
5555
output: {
5656
contacts: data.result || [],
57-
contactCount: data.contact_count,
57+
contactCount: data.contact_count ?? null,
5858
},
5959
}
6060
},
6161

6262
outputs: {
6363
contacts: { type: 'json', description: 'Array of matching contacts' },
64-
contactCount: { type: 'number', description: 'Total number of contacts found' },
64+
contactCount: {
65+
type: 'number',
66+
description: 'Total number of contacts found',
67+
optional: true,
68+
},
6569
},
6670
}

apps/sim/tools/sendgrid/send_mail.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export const sendGridSendMailTool: ToolConfig<SendMailParams, SendMailResult> =
149149

150150
outputs: {
151151
success: { type: 'boolean', description: 'Whether the email was sent successfully' },
152-
messageId: { type: 'string', description: 'SendGrid message ID' },
152+
messageId: { type: 'string', description: 'SendGrid message ID', optional: true },
153153
to: { type: 'string', description: 'Recipient email address' },
154154
subject: { type: 'string', description: 'Email subject' },
155155
},

0 commit comments

Comments
 (0)