Skip to content

Commit 4cde5ce

Browse files
committed
fix(smartlead): correct request params and outputs found in re-validation
Three tools sent a parameter Smartlead's validator rejects outright with 400, so the affected operations failed whenever the field was filled in: - get_campaign_lead_statistics paginated with `skip`; the endpoint accepts `offset` and only echoes it back as `skip`. - list_lead_activities and list_inbox_replies both sent a campaign filter. `campaign_id`, `campaignId`, `campaign_ids` and `email_campaign_id` are all rejected, so the filter is gone rather than advertised and broken. mark_lead_complete reported `next_sequence: null` on every call, including when a step remained: `status.nextSequence` is an object, not a number. It now maps to `next_sequence_id` and `next_sequence_delay_in_days` — verified live returning step 10093171 rather than null. get_lead_by_id reused the by-email mapper, so it always claimed the lead belongs to zero campaigns; `GET /leads/{id}` omits `lead_campaign_data` entirely. It now declares the narrower shape it actually returns. A stale advanced `clientId` leaked into list_email_accounts: advanced subblocks serialize without evaluating their condition, and that tool consumes `clientId` while sitting outside its condition list. The field is now offered for that operation too, so the value is visible wherever it is sent. Two dropdowns had defaults that act on their own. `status` defaulted to PAUSED, so choosing Update Campaign Status and never opening the dropdown paused the campaign; it now requires an explicit choice. `pauseLead` sent `false` on every categorization, which risks resuming a paused lead; it now defaults to leaving the state alone. Also counts CSV export rows with a quote-aware scan so a newline inside a name, location, or custom field no longer inflates the count, and fills in the block output declarations for the fields the 47 tools actually return.
1 parent eab4f75 commit 4cde5ce

12 files changed

Lines changed: 2212 additions & 49 deletions

File tree

apps/docs/content/docs/en/integrations/smartlead.mdx

Lines changed: 2069 additions & 3 deletions
Large diffs are not rendered by default.

apps/sim/blocks/blocks/smartlead.ts

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,13 @@ export const SmartleadBlock: BlockConfig<SmartleadResponse> = {
183183
title: 'Status',
184184
type: 'dropdown',
185185
options: [
186+
{ label: 'Select a status', id: '' },
186187
{ label: 'Start', id: 'START' },
187188
{ label: 'Pause', id: 'PAUSED' },
188189
{ label: 'Stop', id: 'STOPPED' },
189190
],
190-
value: () => 'PAUSED',
191+
// No default: a materialized default would let an untouched dropdown pause a campaign.
192+
value: () => '',
191193
required: { field: 'operation', value: 'update_campaign_status' },
192194
condition: { field: 'operation', value: 'update_campaign_status' },
193195
},
@@ -427,20 +429,15 @@ export const SmartleadBlock: BlockConfig<SmartleadResponse> = {
427429
},
428430
mode: 'advanced',
429431
},
430-
{
431-
id: 'skip',
432-
title: 'Skip',
433-
type: 'short-input',
434-
placeholder: '0',
435-
condition: { field: 'operation', value: 'get_campaign_lead_statistics' },
436-
mode: 'advanced',
437-
},
438432
{
439433
id: 'clientId',
440434
title: 'Client ID',
441435
type: 'short-input',
442436
placeholder: 'Agency client ID',
443-
condition: { field: 'operation', value: ['list_campaigns', 'create_campaign'] },
437+
condition: {
438+
field: 'operation',
439+
value: ['list_campaigns', 'create_campaign', 'list_email_accounts'],
440+
},
444441
mode: 'advanced',
445442
},
446443
{
@@ -581,10 +578,11 @@ export const SmartleadBlock: BlockConfig<SmartleadResponse> = {
581578
title: 'Pause Lead',
582579
type: 'dropdown',
583580
options: [
581+
{ label: 'Leave unchanged', id: '' },
584582
{ label: 'No', id: 'false' },
585583
{ label: 'Yes', id: 'true' },
586584
],
587-
value: () => 'false',
585+
value: () => '',
588586
condition: { field: 'operation', value: 'update_lead_category' },
589587
mode: 'advanced',
590588
},
@@ -821,7 +819,6 @@ export const SmartleadBlock: BlockConfig<SmartleadResponse> = {
821819
fromTime: emptyToUndefined(params.fromTime),
822820
toTime: emptyToUndefined(params.toTime),
823821
unreadOnly: toBooleanParam(params.unreadOnly),
824-
skip: toNumberParam(params.skip),
825822
offset: toNumberParam(params.offset),
826823
limit: toNumberParam(params.limit),
827824
emailSequenceNumber: toNumberParam(params.emailSequenceNumber),
@@ -833,6 +830,9 @@ export const SmartleadBlock: BlockConfig<SmartleadResponse> = {
833830
pauseLead: toBooleanParam(params.pauseLead),
834831
name: params.operation === 'create_campaign' ? params.campaignName : params.webhookName,
835832
email: emptyToUndefined(params.leadEmail),
833+
// Empty means "not chosen", so required validation reports it instead of
834+
// the API rejecting an empty status.
835+
status: emptyToUndefined(params.status),
836836
emailStatus: emptyToUndefined(params.emailStatus),
837837
stopLeadSettings: emptyToUndefined(params.stopLeadSettings),
838838
daysOfTheWeek: parseNumberList(params.daysOfTheWeek),
@@ -929,7 +929,6 @@ export const SmartleadBlock: BlockConfig<SmartleadResponse> = {
929929
emailSequenceNumber: { type: 'number', description: 'Sequence step filter' },
930930
offset: { type: 'number', description: 'Pagination offset' },
931931
limit: { type: 'number', description: 'Pagination limit' },
932-
skip: { type: 'number', description: 'Rows to skip' },
933932
deleteWebhookId: { type: 'number', description: 'Webhook ID to delete' },
934933
fromTime: { type: 'string', description: 'Start of the reporting window' },
935934
toTime: { type: 'string', description: 'End of the reporting window' },
@@ -977,6 +976,56 @@ export const SmartleadBlock: BlockConfig<SmartleadResponse> = {
977976
has_more: { type: 'boolean', description: 'Whether more rows are available' },
978977
list_name: { type: 'string', description: 'Lead list name' },
979978
positive_reply_count: { type: 'number', description: 'Replies categorized as positive' },
979+
offset: { type: 'number', description: 'Pagination offset used' },
980+
limit: { type: 'number', description: 'Pagination limit used' },
981+
total_count: { type: 'number', description: 'Total records matching the request' },
982+
from: { type: 'string', description: 'Start of the reported window' },
983+
to: { type: 'string', description: 'End of the reported window' },
984+
start_date: { type: 'string', description: 'Start of the reported range' },
985+
end_date: { type: 'string', description: 'End of the reported range' },
986+
skipped_count: { type: 'number', description: 'Emails skipped' },
987+
failed_count: { type: 'number', description: 'Failed sends' },
988+
stopped_count: { type: 'number', description: 'Stopped leads' },
989+
unsubscribed_count: { type: 'number', description: 'Unsubscribes' },
990+
unique_sent_count: { type: 'number', description: 'Unique leads emailed' },
991+
unique_open_count: { type: 'number', description: 'Unique opens' },
992+
unique_click_count: { type: 'number', description: 'Unique clicks' },
993+
block_count: {
994+
type: 'number',
995+
description: 'Blocked sends, or leads skipped by the block list',
996+
},
997+
drafted_count: { type: 'number', description: 'Drafted emails' },
998+
sequence_count: { type: 'number', description: 'Sequence steps in the campaign' },
999+
duplicate_count: { type: 'number', description: 'Duplicate leads skipped on import' },
1000+
invalid_email_count: { type: 'number', description: 'Leads skipped for an invalid email' },
1001+
invalid_emails: { type: 'array', description: 'Emails rejected as invalid' },
1002+
already_added_to_campaign: { type: 'number', description: 'Leads already in the campaign' },
1003+
unsubscribed_leads: { type: 'array', description: 'Leads skipped because they unsubscribed' },
1004+
lead_import_stopped_count: { type: 'number', description: 'Leads whose import was stopped' },
1005+
is_lead_limit_exhausted: {
1006+
type: 'boolean',
1007+
description: 'Whether the plan lead limit was hit',
1008+
},
1009+
is_last_sequence: { type: 'boolean', description: 'Whether the lead was on the final step' },
1010+
next_sequence_id: { type: 'number', description: 'ID of the next sequence step' },
1011+
next_sequence_delay_in_days: { type: 'number', description: 'Days before the next step' },
1012+
first_name: { type: 'string', description: 'Lead first name' },
1013+
last_name: { type: 'string', description: 'Lead last name' },
1014+
phone_number: { type: 'string', description: 'Lead phone number' },
1015+
company_name: { type: 'string', description: 'Lead company name' },
1016+
website: { type: 'string', description: 'Lead website' },
1017+
location: { type: 'string', description: 'Lead location' },
1018+
linkedin_profile: { type: 'string', description: 'Lead LinkedIn profile URL' },
1019+
company_url: { type: 'string', description: 'Lead company URL' },
1020+
custom_fields: { type: 'json', description: 'Lead custom fields' },
1021+
is_unsubscribed: { type: 'boolean', description: 'Whether the lead is unsubscribed' },
1022+
lead_campaign_data: { type: 'array', description: 'Campaigns the lead belongs to' },
1023+
created_at: { type: 'string', description: 'Creation timestamp' },
1024+
updated_at: { type: 'string', description: 'Last update timestamp' },
1025+
leads_count: { type: 'number', description: 'Leads in the list' },
1026+
active_leads_count: { type: 'number', description: 'Active leads in the list' },
1027+
track_settings: { type: 'array', description: 'Disabled tracking settings' },
1028+
scheduler_cron_value: { type: 'json', description: 'Campaign sending schedule' },
9801029
},
9811030
}
9821031

apps/sim/tools/generated/tool-metadata.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/generated/tool-outputs.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/smartlead/export_campaign_leads.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,39 @@ import {
1212
} from '@/tools/smartlead/utils'
1313
import type { ToolConfig } from '@/tools/types'
1414

15+
/**
16+
* Counts data rows excluding the header, treating newlines inside quoted fields
17+
* as part of the value — lead names, locations, and custom fields can contain them.
18+
*/
19+
function countCsvDataRows(csv: string): number {
20+
let rows = 0
21+
let inQuotes = false
22+
let lineHasContent = false
23+
24+
for (let i = 0; i < csv.length; i++) {
25+
const char = csv[i]
26+
if (char === '"') {
27+
// A doubled quote inside a quoted field is an escaped literal quote.
28+
if (inQuotes && csv[i + 1] === '"') {
29+
i++
30+
continue
31+
}
32+
inQuotes = !inQuotes
33+
lineHasContent = true
34+
continue
35+
}
36+
if (char === '\n' && !inQuotes) {
37+
if (lineHasContent) rows++
38+
lineHasContent = false
39+
continue
40+
}
41+
if (char !== '\r') lineHasContent = true
42+
}
43+
if (lineHasContent) rows++
44+
45+
return Math.max(0, rows - 1)
46+
}
47+
1548
export const exportCampaignLeadsTool: ToolConfig<
1649
SmartleadCampaignIdParams,
1750
SmartleadExportLeadsResponse
@@ -34,14 +67,12 @@ export const exportCampaignLeadsTool: ToolConfig<
3467
},
3568
transformResponse: async (response) => {
3669
const csv = await response.text()
37-
// The first line is the header row, so data rows are the remaining non-empty lines.
38-
const rowCount = csv.split('\n').filter((line) => line.trim() !== '').length
3970

4071
return {
4172
success: true,
4273
output: {
4374
csv,
44-
row_count: Math.max(0, rowCount - 1),
75+
row_count: countCsvDataRows(csv),
4576
},
4677
}
4778
},

apps/sim/tools/smartlead/get_campaign_lead_statistics.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type { ToolConfig } from '@/tools/types'
1717

1818
interface GetCampaignLeadStatisticsParams extends SmartleadCampaignIdParams {
1919
limit?: number
20-
skip?: number
20+
offset?: number
2121
}
2222

2323
export const getCampaignLeadStatisticsTool: ToolConfig<
@@ -38,18 +38,19 @@ export const getCampaignLeadStatisticsTool: ToolConfig<
3838
visibility: 'user-or-llm',
3939
description: 'Rows to return (default 100)',
4040
},
41-
skip: {
41+
offset: {
4242
type: 'number',
4343
required: false,
4444
visibility: 'user-or-llm',
4545
description: 'Rows to skip (default 0)',
4646
},
4747
},
4848
request: {
49+
// Smartlead takes `offset` here but echoes it back as `skip`.
4950
url: (params) =>
5051
smartleadUrl(`/campaigns/${pathSegment(params.campaignId)}/leads-statistics`, params.apiKey, {
5152
limit: params.limit,
52-
skip: params.skip,
53+
offset: params.offset,
5354
}),
5455
method: 'GET',
5556
headers: smartleadHeaders,

apps/sim/tools/smartlead/get_lead_by_id.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { ErrorExtractorId } from '@/tools/error-extractors'
22
import type { SmartleadBaseParams, SmartleadLeadByIdResponse } from '@/tools/smartlead/types'
33
import {
4-
leadDetailOutputs,
5-
mapLeadDetail,
4+
leadRecordOutputs,
5+
mapLead,
66
pathSegment,
77
smartleadBaseParamFields,
88
smartleadHeaders,
@@ -41,11 +41,15 @@ export const getLeadByIdTool: ToolConfig<GetLeadByIdParams, SmartleadLeadByIdRes
4141
// This endpoint wraps the lead in a single-element `data` array.
4242
const rows = Array.isArray(record.data) ? record.data : []
4343
if (rows.length === 0) throw new Error('Smartlead lead not found')
44+
const lead = rows[0] as Record<string, unknown>
4445

4546
return {
4647
success: true,
47-
output: mapLeadDetail(rows[0]),
48+
output: {
49+
...mapLead(lead),
50+
created_at: typeof lead.created_at === 'string' ? lead.created_at : null,
51+
},
4852
}
4953
},
50-
outputs: leadDetailOutputs,
54+
outputs: leadRecordOutputs,
5155
}

apps/sim/tools/smartlead/list_inbox_replies.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ import {
1111
} from '@/tools/smartlead/utils'
1212
import type { ToolConfig } from '@/tools/types'
1313

14+
/**
15+
* Smartlead rejects every campaign-filter key this endpoint might plausibly take
16+
* (`campaign_id`, `campaign_ids`, `campaignIds`), so only pagination is offered.
17+
*/
1418
interface ListInboxRepliesParams extends SmartleadBaseParams {
1519
offset?: number
1620
limit?: number
17-
campaignId?: number
1821
unreadOnly?: boolean
1922
}
2023

@@ -36,12 +39,6 @@ export const listInboxRepliesTool: ToolConfig<
3639
visibility: 'user-or-llm',
3740
description: 'Return only unread replies',
3841
},
39-
campaignId: {
40-
type: 'number',
41-
required: false,
42-
visibility: 'user-or-llm',
43-
description: 'Only return replies for this campaign',
44-
},
4542
offset: {
4643
type: 'number',
4744
required: false,
@@ -67,7 +64,6 @@ export const listInboxRepliesTool: ToolConfig<
6764
jsonBody({
6865
offset: params.offset ?? 0,
6966
limit: params.limit,
70-
campaign_id: params.campaignId,
7167
}),
7268
},
7369
transformResponse: async (response) => {

apps/sim/tools/smartlead/list_lead_activities.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ import {
1111
} from '@/tools/smartlead/utils'
1212
import type { ToolConfig } from '@/tools/types'
1313

14+
/**
15+
* Smartlead exposes no working campaign filter on this endpoint — `campaign_id`,
16+
* `campaignId`, `campaign_ids` and `email_campaign_id` are all rejected by its
17+
* validator, so only pagination is offered.
18+
*/
1419
interface ListLeadActivitiesParams extends SmartleadBaseParams {
1520
offset?: number
1621
limit?: number
17-
campaignId?: number
1822
}
1923

2024
export const listLeadActivitiesTool: ToolConfig<
@@ -29,12 +33,6 @@ export const listLeadActivitiesTool: ToolConfig<
2933
errorExtractor: ErrorExtractorId.SMARTLEAD_ERRORS,
3034
params: {
3135
...smartleadBaseParamFields,
32-
campaignId: {
33-
type: 'number',
34-
required: false,
35-
visibility: 'user-or-llm',
36-
description: 'Only return activity for this campaign',
37-
},
3836
offset: {
3937
type: 'number',
4038
required: false,
@@ -53,7 +51,6 @@ export const listLeadActivitiesTool: ToolConfig<
5351
smartleadUrl('/campaigns/all-leads-activities', params.apiKey, {
5452
offset: params.offset,
5553
limit: params.limit,
56-
campaign_id: params.campaignId,
5754
}),
5855
method: 'GET',
5956
headers: smartleadHeaders,

apps/sim/tools/smartlead/mark_lead_complete.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,16 @@ export const markLeadCompleteTool: ToolConfig<
5454
transformResponse: async (response) => {
5555
const record = await smartleadRecord(response, 'lead completion')
5656
const status = isRecordLike(record.status) ? record.status : {}
57+
// `nextSequence` is an object ({ id, delayInDays }) when a step remains, else null.
58+
const next = isRecordLike(status.nextSequence) ? status.nextSequence : null
5759

5860
return {
5961
success: true,
6062
output: {
6163
success: isOk(record),
6264
is_last_sequence: typeof status.isLastSequence === 'boolean' ? status.isLastSequence : null,
63-
next_sequence: typeof status.nextSequence === 'number' ? status.nextSequence : null,
65+
next_sequence_id: next && typeof next.id === 'number' ? next.id : null,
66+
next_sequence_delay_in_days: next ? Number(next.delayInDays) || null : null,
6467
},
6568
}
6669
},

0 commit comments

Comments
 (0)