improvement(instantly): validate integration against live API, add lead/campaign lifecycle tools (#5448)

* improvement(instantly): validate integration against live API, add lead/campaign lifecycle tools

- Verify all existing Instantly tools against the live API v2 spec
- Add instantly_patch_lead, instantly_pause_campaign, instantly_delete_campaign
- Fix activate_campaign response mapping to surface the full campaign object
- Restore status/ai_sales_agent_id list_campaigns filters

* fix(instantly): guard against empty patch_lead request body

Follows the same pattern used in tools/langsmith/update_run.ts.
This commit is contained in:
Waleed
2026-07-06 15:34:00 -07:00
committed by GitHub
parent 0363451539
commit 0916b193d9
10 changed files with 350 additions and 20 deletions
+44 -14
View File
@@ -6,13 +6,23 @@ import type { InstantlyResponse } from '@/tools/instantly/types'
import { getTrigger } from '@/triggers'
const LEAD_LIST_OPERATIONS = ['list_leads'] as const
const LEAD_ID_OPERATIONS = ['get_lead'] as const
const LEAD_ID_OPERATIONS = ['get_lead', 'patch_lead'] as const
const LEAD_CREATE_OPERATIONS = ['create_lead'] as const
const LEAD_PATCH_OPERATIONS = ['patch_lead'] as const
const LEAD_MUTATION_FIELD_OPERATIONS = [
...LEAD_CREATE_OPERATIONS,
...LEAD_PATCH_OPERATIONS,
] as const
const LEAD_DELETE_OPERATIONS = ['delete_leads'] as const
const LEAD_INTEREST_OPERATIONS = ['update_lead_interest_status'] as const
const CAMPAIGN_LIST_OPERATIONS = ['list_campaigns'] as const
const CAMPAIGN_MUTATION_OPERATIONS = ['create_campaign', 'patch_campaign'] as const
const CAMPAIGN_ID_OPERATIONS = ['patch_campaign', 'activate_campaign'] as const
const CAMPAIGN_ID_OPERATIONS = [
'patch_campaign',
'activate_campaign',
'pause_campaign',
'delete_campaign',
] as const
const EMAIL_LIST_OPERATIONS = ['list_emails'] as const
const EMAIL_REPLY_OPERATIONS = ['reply_to_email'] as const
const LEAD_LIST_LIST_OPERATIONS = ['list_lead_lists'] as const
@@ -51,7 +61,7 @@ export const InstantlyBlock: BlockConfig<InstantlyResponse> = {
name: 'Instantly',
description: 'Manage Instantly leads, campaigns, emails, and lead lists',
longDescription:
'Integrate Instantly API V2 into workflows. Create and list leads, manage lead interest status, delete leads in bulk, list and create campaigns, reply to emails, and manage lead lists.',
'Integrate Instantly API V2 into workflows. Create, update, and list leads, manage lead interest status, delete leads in bulk, list, create, patch, activate, pause, and delete campaigns, reply to emails, and manage lead lists.',
docsLink: 'https://docs.sim.ai/integrations/instantly',
category: 'tools',
integrationType: IntegrationType.Email,
@@ -67,12 +77,15 @@ export const InstantlyBlock: BlockConfig<InstantlyResponse> = {
{ label: 'List Leads', id: 'list_leads' },
{ label: 'Get Lead', id: 'get_lead' },
{ label: 'Create Lead', id: 'create_lead' },
{ label: 'Patch Lead', id: 'patch_lead' },
{ label: 'Delete Leads', id: 'delete_leads' },
{ label: 'Update Lead Interest Status', id: 'update_lead_interest_status' },
{ label: 'List Campaigns', id: 'list_campaigns' },
{ label: 'Create Campaign', id: 'create_campaign' },
{ label: 'Patch Campaign', id: 'patch_campaign' },
{ label: 'Activate Campaign', id: 'activate_campaign' },
{ label: 'Pause Campaign', id: 'pause_campaign' },
{ label: 'Delete Campaign', id: 'delete_campaign' },
{ label: 'List Emails', id: 'list_emails' },
{ label: 'Reply To Email', id: 'reply_to_email' },
{ label: 'List Lead Lists', id: 'list_lead_lists' },
@@ -134,28 +147,28 @@ export const InstantlyBlock: BlockConfig<InstantlyResponse> = {
title: 'First Name',
type: 'short-input',
placeholder: 'Jane',
condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] },
condition: { field: 'operation', value: [...LEAD_MUTATION_FIELD_OPERATIONS] },
},
{
id: 'lastName',
title: 'Last Name',
type: 'short-input',
placeholder: 'Doe',
condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] },
condition: { field: 'operation', value: [...LEAD_MUTATION_FIELD_OPERATIONS] },
},
{
id: 'companyName',
title: 'Company Name',
type: 'short-input',
placeholder: 'Acme Inc.',
condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] },
condition: { field: 'operation', value: [...LEAD_MUTATION_FIELD_OPERATIONS] },
},
{
id: 'jobTitle',
title: 'Job Title',
type: 'short-input',
placeholder: 'Head of Growth',
condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] },
condition: { field: 'operation', value: [...LEAD_MUTATION_FIELD_OPERATIONS] },
mode: 'advanced',
},
{
@@ -163,7 +176,7 @@ export const InstantlyBlock: BlockConfig<InstantlyResponse> = {
title: 'Phone',
type: 'short-input',
placeholder: '+1234567890',
condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] },
condition: { field: 'operation', value: [...LEAD_MUTATION_FIELD_OPERATIONS] },
mode: 'advanced',
},
{
@@ -171,7 +184,7 @@ export const InstantlyBlock: BlockConfig<InstantlyResponse> = {
title: 'Website',
type: 'short-input',
placeholder: 'https://example.com',
condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] },
condition: { field: 'operation', value: [...LEAD_MUTATION_FIELD_OPERATIONS] },
mode: 'advanced',
},
{
@@ -179,7 +192,7 @@ export const InstantlyBlock: BlockConfig<InstantlyResponse> = {
title: 'Personalization',
type: 'long-input',
placeholder: 'Personalized opening line',
condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] },
condition: { field: 'operation', value: [...LEAD_MUTATION_FIELD_OPERATIONS] },
mode: 'advanced',
},
{
@@ -187,7 +200,7 @@ export const InstantlyBlock: BlockConfig<InstantlyResponse> = {
title: 'Custom Variables',
type: 'long-input',
placeholder: '{"past_customer": true, "industry": "SaaS"}',
condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] },
condition: { field: 'operation', value: [...LEAD_MUTATION_FIELD_OPERATIONS] },
wandConfig: {
enabled: true,
prompt:
@@ -266,7 +279,7 @@ export const InstantlyBlock: BlockConfig<InstantlyResponse> = {
title: 'Lead Interest Status',
type: 'short-input',
placeholder: '1',
condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] },
condition: { field: 'operation', value: [...LEAD_MUTATION_FIELD_OPERATIONS] },
mode: 'advanced',
},
{
@@ -274,7 +287,7 @@ export const InstantlyBlock: BlockConfig<InstantlyResponse> = {
title: 'Potential Lead Value',
type: 'short-input',
placeholder: 'High',
condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] },
condition: { field: 'operation', value: [...LEAD_MUTATION_FIELD_OPERATIONS] },
mode: 'advanced',
},
{
@@ -282,7 +295,7 @@ export const InstantlyBlock: BlockConfig<InstantlyResponse> = {
title: 'Assigned To',
type: 'short-input',
placeholder: 'Organization user UUID',
condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] },
condition: { field: 'operation', value: [...LEAD_MUTATION_FIELD_OPERATIONS] },
mode: 'advanced',
},
{
@@ -842,12 +855,15 @@ export const InstantlyBlock: BlockConfig<InstantlyResponse> = {
'instantly_list_leads',
'instantly_get_lead',
'instantly_create_lead',
'instantly_patch_lead',
'instantly_delete_leads',
'instantly_update_lead_interest_status',
'instantly_list_campaigns',
'instantly_create_campaign',
'instantly_patch_campaign',
'instantly_activate_campaign',
'instantly_pause_campaign',
'instantly_delete_campaign',
'instantly_list_emails',
'instantly_reply_to_email',
'instantly_list_lead_lists',
@@ -1350,5 +1366,19 @@ export const InstantlyBlockMeta = {
content:
'# Campaign Performance Snapshot\n\nGive a quick read on how outreach is going across campaigns.\n\n## Steps\n1. List campaigns and note which are active.\n2. For each active campaign, list leads and tally counts by interest status.\n3. Pull recent emails to gauge reply volume.\n\n## Output\nReturn a per-campaign snapshot: total leads, breakdown by interest status, and reply activity, highlighting the top-performing campaign.',
},
{
name: 'clean-up-lead-data',
description:
'Correct or enrich existing lead records so personalization and CRM sync stay accurate.',
content:
'# Clean Up Lead Data\n\nKeep lead records accurate after enrichment, verification, or manual review.\n\n## Steps\n1. Look up the lead by ID or email.\n2. Compare the current fields against the corrected or enriched data.\n3. Patch the lead with the updated first name, last name, company, job title, phone, website, or custom variables.\n\n## Output\nReturn the updated lead record and a summary of which fields changed.',
},
{
name: 'pause-underperforming-campaigns',
description:
'Detect campaigns with poor reply or interest rates and pause them to protect sender reputation.',
content:
'# Pause Underperforming Campaigns\n\nProtect domain and inbox reputation by pausing cold-email campaigns that are not converting.\n\n## Steps\n1. List active campaigns.\n2. For each campaign, list its leads and tally reply and interest counts.\n3. Flag campaigns below a reply-rate threshold.\n4. Pause each flagged campaign so no further emails are sent.\n\n## Output\nReturn which campaigns were paused, their reply rates, and the campaigns left active for comparison.',
},
],
} as const satisfies BlockMeta
@@ -1,9 +1,10 @@
import type {
InstantlyActivateCampaignParams,
InstantlyCampaignResponse,
InstantlyCampaignActionResponse,
} from '@/tools/instantly/types'
import {
campaignOutputs,
campaignActionOutputs,
getMessage,
instantlyBaseParamFields,
instantlyHeaders,
instantlyUrl,
@@ -14,7 +15,7 @@ import type { ToolConfig } from '@/tools/types'
export const activateCampaignTool: ToolConfig<
InstantlyActivateCampaignParams,
InstantlyCampaignResponse
InstantlyCampaignActionResponse
> = {
id: 'instantly_activate_campaign',
name: 'Instantly Activate Campaign',
@@ -45,8 +46,9 @@ export const activateCampaignTool: ToolConfig<
id: campaign.id,
name: campaign.name,
status: campaign.status,
message: getMessage(data),
},
}
},
outputs: campaignOutputs,
outputs: campaignActionOutputs,
}
@@ -0,0 +1,54 @@
import type {
InstantlyCampaignActionResponse,
InstantlyDeleteCampaignParams,
} from '@/tools/instantly/types'
import {
campaignActionOutputs,
getMessage,
instantlyBaseParamFields,
instantlyHeaders,
instantlyUrl,
mapCampaign,
parseInstantlyResponse,
} from '@/tools/instantly/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteCampaignTool: ToolConfig<
InstantlyDeleteCampaignParams,
InstantlyCampaignActionResponse
> = {
id: 'instantly_delete_campaign',
name: 'Instantly Delete Campaign',
description: 'Permanently deletes an Instantly V2 campaign.',
version: '1.0.0',
params: {
...instantlyBaseParamFields,
campaignId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Campaign ID',
},
},
request: {
url: (params) => instantlyUrl(`/api/v2/campaigns/${params.campaignId.trim()}`),
method: 'DELETE',
headers: instantlyHeaders,
},
transformResponse: async (response) => {
const data = await parseInstantlyResponse(response)
const campaign = mapCampaign(data)
return {
success: true,
output: {
campaign,
id: campaign.id,
name: campaign.name,
status: campaign.status,
message: getMessage(data),
},
}
},
outputs: campaignActionOutputs,
}
+3
View File
@@ -2,6 +2,7 @@ export { activateCampaignTool as instantlyActivateCampaignTool } from '@/tools/i
export { createCampaignTool as instantlyCreateCampaignTool } from '@/tools/instantly/create_campaign'
export { createLeadTool as instantlyCreateLeadTool } from '@/tools/instantly/create_lead'
export { createLeadListTool as instantlyCreateLeadListTool } from '@/tools/instantly/create_lead_list'
export { deleteCampaignTool as instantlyDeleteCampaignTool } from '@/tools/instantly/delete_campaign'
export { deleteLeadsTool as instantlyDeleteLeadsTool } from '@/tools/instantly/delete_leads'
export { getLeadTool as instantlyGetLeadTool } from '@/tools/instantly/get_lead'
export { listCampaignsTool as instantlyListCampaignsTool } from '@/tools/instantly/list_campaigns'
@@ -9,6 +10,8 @@ export { listEmailsTool as instantlyListEmailsTool } from '@/tools/instantly/lis
export { listLeadListsTool as instantlyListLeadListsTool } from '@/tools/instantly/list_lead_lists'
export { listLeadsTool as instantlyListLeadsTool } from '@/tools/instantly/list_leads'
export { patchCampaignTool as instantlyPatchCampaignTool } from '@/tools/instantly/patch_campaign'
export { patchLeadTool as instantlyPatchLeadTool } from '@/tools/instantly/patch_lead'
export { pauseCampaignTool as instantlyPauseCampaignTool } from '@/tools/instantly/pause_campaign'
export { replyToEmailTool as instantlyReplyToEmailTool } from '@/tools/instantly/reply_to_email'
export * from '@/tools/instantly/types'
export { updateLeadInterestStatusTool as instantlyUpdateLeadInterestStatusTool } from '@/tools/instantly/update_lead_interest_status'
+1 -1
View File
@@ -52,7 +52,7 @@ export const listCampaignsTool: ToolConfig<
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'AI Sales Agent ID filter',
description: 'Filter campaigns by AI Sales Agent ID',
},
status: {
type: 'number',
+137
View File
@@ -0,0 +1,137 @@
import type { InstantlyLeadResponse, InstantlyPatchLeadParams } from '@/tools/instantly/types'
import {
compactBody,
instantlyBaseParamFields,
instantlyHeaders,
instantlyUrl,
leadOutputs,
mapLead,
parseInstantlyResponse,
} from '@/tools/instantly/utils'
import type { ToolConfig } from '@/tools/types'
export const patchLeadTool: ToolConfig<InstantlyPatchLeadParams, InstantlyLeadResponse> = {
id: 'instantly_patch_lead',
name: 'Instantly Patch Lead',
description: 'Updates fields on an existing Instantly V2 lead.',
version: '1.0.0',
params: {
...instantlyBaseParamFields,
leadId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Lead ID',
},
first_name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead first name',
},
last_name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead last name',
},
company_name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead company name',
},
job_title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead job title',
},
phone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead phone number',
},
website: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead website',
},
personalization: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lead personalization text',
},
lt_interest_status: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Lead interest status value',
},
pl_value_lead: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Potential value of the lead',
},
assigned_to: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ID of the user assigned to the lead',
},
custom_variables: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Custom variable object with string, number, boolean, or null values',
},
},
request: {
url: (params) => instantlyUrl(`/api/v2/leads/${params.leadId.trim()}`),
method: 'PATCH',
headers: instantlyHeaders,
body: (params) => {
const body = compactBody({
first_name: params.first_name,
last_name: params.last_name,
company_name: params.company_name,
job_title: params.job_title,
phone: params.phone,
website: params.website,
personalization: params.personalization,
lt_interest_status: params.lt_interest_status,
pl_value_lead: params.pl_value_lead,
assigned_to: params.assigned_to,
custom_variables: params.custom_variables,
})
if (Object.keys(body).length === 0) {
throw new Error('Provide at least one field to update')
}
return body
},
},
transformResponse: async (response) => {
const data = await parseInstantlyResponse(response)
const lead = mapLead(data)
return {
success: true,
output: {
lead,
id: lead.id,
email_address: lead.email,
first_name: lead.first_name,
last_name: lead.last_name,
campaign: lead.campaign,
status: lead.status,
},
}
},
outputs: leadOutputs,
}
@@ -0,0 +1,54 @@
import type {
InstantlyCampaignActionResponse,
InstantlyPauseCampaignParams,
} from '@/tools/instantly/types'
import {
campaignActionOutputs,
getMessage,
instantlyBaseParamFields,
instantlyHeaders,
instantlyUrl,
mapCampaign,
parseInstantlyResponse,
} from '@/tools/instantly/utils'
import type { ToolConfig } from '@/tools/types'
export const pauseCampaignTool: ToolConfig<
InstantlyPauseCampaignParams,
InstantlyCampaignActionResponse
> = {
id: 'instantly_pause_campaign',
name: 'Instantly Pause Campaign',
description: 'Pauses a running Instantly V2 campaign, stopping further email sends.',
version: '1.0.0',
params: {
...instantlyBaseParamFields,
campaignId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Campaign ID',
},
},
request: {
url: (params) => instantlyUrl(`/api/v2/campaigns/${params.campaignId.trim()}/pause`),
method: 'POST',
headers: instantlyHeaders,
},
transformResponse: async (response) => {
const data = await parseInstantlyResponse(response)
const campaign = mapCampaign(data)
return {
success: true,
output: {
campaign,
id: campaign.id,
name: campaign.name,
status: campaign.status,
message: getMessage(data),
},
}
},
outputs: campaignActionOutputs,
}
+35 -1
View File
@@ -109,6 +109,21 @@ export interface InstantlyGetLeadParams extends InstantlyBaseParams {
leadId: string
}
export interface InstantlyPatchLeadParams extends InstantlyBaseParams {
leadId: string
personalization?: string | null
website?: string | null
last_name?: string | null
first_name?: string | null
company_name?: string | null
job_title?: string | null
phone?: string | null
lt_interest_status?: number
pl_value_lead?: string | null
assigned_to?: string | null
custom_variables?: Record<string, InstantlyScalar>
}
export interface InstantlyCreateLeadParams extends InstantlyBaseParams {
campaign?: string | null
email?: string | null
@@ -142,7 +157,7 @@ export interface InstantlyDeleteLeadsParams extends InstantlyBaseParams {
export interface InstantlyUpdateLeadInterestStatusParams extends InstantlyBaseParams {
lead_email: string
interest_value: number | null
interest_value?: number | null
campaign_id?: string
ai_interest_value?: number
disable_auto_interest?: boolean
@@ -182,6 +197,14 @@ export interface InstantlyActivateCampaignParams extends InstantlyBaseParams {
campaignId: string
}
export interface InstantlyPauseCampaignParams extends InstantlyBaseParams {
campaignId: string
}
export interface InstantlyDeleteCampaignParams extends InstantlyBaseParams {
campaignId: string
}
export interface InstantlyListEmailsParams extends InstantlyBaseParams {
limit?: number
starting_after?: string
@@ -268,6 +291,16 @@ export interface InstantlyCampaignResponse extends ToolResponse {
}
}
export interface InstantlyCampaignActionResponse extends ToolResponse {
output: {
campaign: InstantlyCampaign
id: string | null
name: string | null
status: number | null
message: string | null
}
}
export interface InstantlyListEmailsResponse extends ToolResponse {
output: {
emails: InstantlyEmail[]
@@ -308,6 +341,7 @@ export type InstantlyResponse =
| InstantlyUpdateLeadInterestStatusResponse
| InstantlyListCampaignsResponse
| InstantlyCampaignResponse
| InstantlyCampaignActionResponse
| InstantlyListEmailsResponse
| InstantlyEmailResponse
| InstantlyListLeadListsResponse
+10
View File
@@ -232,6 +232,11 @@ export const campaignOutputs = {
status: { type: 'number', description: 'Campaign status', optional: true },
} satisfies ToolConfig['outputs']
export const campaignActionOutputs = {
...campaignOutputs,
message: { type: 'string', description: 'Confirmation message from Instantly', optional: true },
} satisfies ToolConfig['outputs']
export const campaignsListOutputs = {
campaigns: {
type: 'array',
@@ -342,6 +347,11 @@ async function parseJsonResponse(response: Response): Promise<unknown> {
}
}
export function getMessage(value: unknown): string | null {
const data = asRecord(value)
return typeof data.message === 'string' ? data.message : null
}
function extractInstantlyError(value: unknown, fallback: string): string {
const data = asRecord(value)
if (typeof data.message === 'string') return data.message
+6
View File
@@ -1743,6 +1743,7 @@ import {
instantlyCreateCampaignTool,
instantlyCreateLeadListTool,
instantlyCreateLeadTool,
instantlyDeleteCampaignTool,
instantlyDeleteLeadsTool,
instantlyGetLeadTool,
instantlyListCampaignsTool,
@@ -1750,6 +1751,8 @@ import {
instantlyListLeadListsTool,
instantlyListLeadsTool,
instantlyPatchCampaignTool,
instantlyPatchLeadTool,
instantlyPauseCampaignTool,
instantlyReplyToEmailTool,
instantlyUpdateLeadInterestStatusTool,
} from '@/tools/instantly'
@@ -4672,6 +4675,7 @@ export const tools: Record<string, ToolConfig> = {
instantly_create_campaign: instantlyCreateCampaignTool,
instantly_create_lead: instantlyCreateLeadTool,
instantly_create_lead_list: instantlyCreateLeadListTool,
instantly_delete_campaign: instantlyDeleteCampaignTool,
instantly_delete_leads: instantlyDeleteLeadsTool,
instantly_get_lead: instantlyGetLeadTool,
instantly_list_campaigns: instantlyListCampaignsTool,
@@ -4679,6 +4683,8 @@ export const tools: Record<string, ToolConfig> = {
instantly_list_lead_lists: instantlyListLeadListsTool,
instantly_list_leads: instantlyListLeadsTool,
instantly_patch_campaign: instantlyPatchCampaignTool,
instantly_patch_lead: instantlyPatchLeadTool,
instantly_pause_campaign: instantlyPauseCampaignTool,
instantly_reply_to_email: instantlyReplyToEmailTool,
instantly_update_lead_interest_status: instantlyUpdateLeadInterestStatusTool,
jina_read_url: jinaReadUrlTool,