From 79b8f57f6ee2a8362548919ae89ed6d0287cebb9 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 27 May 2026 15:23:44 -0700 Subject: [PATCH 1/5] feat(instantly): block, trigger --- apps/docs/components/icons.tsx | 12 + apps/docs/components/ui/icon-mapping.ts | 2 + apps/docs/content/docs/en/tools/instantly.mdx | 522 +++++++++++ apps/docs/content/docs/en/tools/meta.json | 1 + .../integrations/data/icon-mapping.ts | 2 + .../integrations/data/integrations.json | 172 ++++ apps/sim/blocks/blocks/instantly.ts | 849 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 12 + apps/sim/lib/webhooks/providers/instantly.ts | 259 ++++++ apps/sim/lib/webhooks/providers/registry.ts | 2 + apps/sim/tools/instantly/activate_campaign.ts | 51 ++ apps/sim/tools/instantly/create_campaign.ts | 150 ++++ apps/sim/tools/instantly/create_lead.ts | 180 ++++ apps/sim/tools/instantly/create_lead_list.ts | 69 ++ apps/sim/tools/instantly/delete_leads.ts | 81 ++ apps/sim/tools/instantly/get_lead.ts | 48 + apps/sim/tools/instantly/index.ts | 14 + apps/sim/tools/instantly/list_campaigns.ts | 90 ++ apps/sim/tools/instantly/list_emails.ts | 115 +++ apps/sim/tools/instantly/list_lead_lists.ts | 76 ++ apps/sim/tools/instantly/list_leads.ts | 148 +++ apps/sim/tools/instantly/patch_campaign.ts | 156 ++++ apps/sim/tools/instantly/reply_to_email.ts | 85 ++ apps/sim/tools/instantly/types.ts | 331 +++++++ .../instantly/update_lead_interest_status.ts | 93 ++ apps/sim/tools/instantly/utils.ts | 339 +++++++ apps/sim/tools/registry.ts | 28 + apps/sim/triggers/instantly/account_error.ts | 8 + .../triggers/instantly/auto_reply_received.ts | 8 + .../triggers/instantly/campaign_completed.ts | 8 + apps/sim/triggers/instantly/email_bounced.ts | 8 + apps/sim/triggers/instantly/email_opened.ts | 8 + apps/sim/triggers/instantly/email_sent.ts | 8 + apps/sim/triggers/instantly/index.ts | 20 + apps/sim/triggers/instantly/lead_closed.ts | 8 + .../sim/triggers/instantly/lead_interested.ts | 8 + .../triggers/instantly/lead_meeting_booked.ts | 8 + .../instantly/lead_meeting_completed.ts | 8 + apps/sim/triggers/instantly/lead_neutral.ts | 8 + apps/sim/triggers/instantly/lead_no_show.ts | 8 + .../triggers/instantly/lead_not_interested.ts | 8 + .../triggers/instantly/lead_out_of_office.ts | 8 + .../triggers/instantly/lead_unsubscribed.ts | 8 + .../triggers/instantly/lead_wrong_person.ts | 8 + apps/sim/triggers/instantly/link_clicked.ts | 8 + apps/sim/triggers/instantly/reply_received.ts | 8 + .../supersearch_enrichment_completed.ts | 8 + apps/sim/triggers/instantly/trigger.ts | 43 + apps/sim/triggers/instantly/utils.ts | 152 ++++ apps/sim/triggers/instantly/webhook.ts | 9 + apps/sim/triggers/registry.ts | 42 + 52 files changed, 4307 insertions(+) create mode 100644 apps/docs/content/docs/en/tools/instantly.mdx create mode 100644 apps/sim/blocks/blocks/instantly.ts create mode 100644 apps/sim/lib/webhooks/providers/instantly.ts create mode 100644 apps/sim/tools/instantly/activate_campaign.ts create mode 100644 apps/sim/tools/instantly/create_campaign.ts create mode 100644 apps/sim/tools/instantly/create_lead.ts create mode 100644 apps/sim/tools/instantly/create_lead_list.ts create mode 100644 apps/sim/tools/instantly/delete_leads.ts create mode 100644 apps/sim/tools/instantly/get_lead.ts create mode 100644 apps/sim/tools/instantly/index.ts create mode 100644 apps/sim/tools/instantly/list_campaigns.ts create mode 100644 apps/sim/tools/instantly/list_emails.ts create mode 100644 apps/sim/tools/instantly/list_lead_lists.ts create mode 100644 apps/sim/tools/instantly/list_leads.ts create mode 100644 apps/sim/tools/instantly/patch_campaign.ts create mode 100644 apps/sim/tools/instantly/reply_to_email.ts create mode 100644 apps/sim/tools/instantly/types.ts create mode 100644 apps/sim/tools/instantly/update_lead_interest_status.ts create mode 100644 apps/sim/tools/instantly/utils.ts create mode 100644 apps/sim/triggers/instantly/account_error.ts create mode 100644 apps/sim/triggers/instantly/auto_reply_received.ts create mode 100644 apps/sim/triggers/instantly/campaign_completed.ts create mode 100644 apps/sim/triggers/instantly/email_bounced.ts create mode 100644 apps/sim/triggers/instantly/email_opened.ts create mode 100644 apps/sim/triggers/instantly/email_sent.ts create mode 100644 apps/sim/triggers/instantly/index.ts create mode 100644 apps/sim/triggers/instantly/lead_closed.ts create mode 100644 apps/sim/triggers/instantly/lead_interested.ts create mode 100644 apps/sim/triggers/instantly/lead_meeting_booked.ts create mode 100644 apps/sim/triggers/instantly/lead_meeting_completed.ts create mode 100644 apps/sim/triggers/instantly/lead_neutral.ts create mode 100644 apps/sim/triggers/instantly/lead_no_show.ts create mode 100644 apps/sim/triggers/instantly/lead_not_interested.ts create mode 100644 apps/sim/triggers/instantly/lead_out_of_office.ts create mode 100644 apps/sim/triggers/instantly/lead_unsubscribed.ts create mode 100644 apps/sim/triggers/instantly/lead_wrong_person.ts create mode 100644 apps/sim/triggers/instantly/link_clicked.ts create mode 100644 apps/sim/triggers/instantly/reply_received.ts create mode 100644 apps/sim/triggers/instantly/supersearch_enrichment_completed.ts create mode 100644 apps/sim/triggers/instantly/trigger.ts create mode 100644 apps/sim/triggers/instantly/utils.ts create mode 100644 apps/sim/triggers/instantly/webhook.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index cd326d6220a..725bad45557 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -415,6 +415,18 @@ export function MailIcon(props: SVGProps) { ) } +export function InstantlyIcon(props: SVGProps) { + return ( + + + + + ) +} + export function EmailBisonIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 3f27a66e0c9..29ff65d4eb1 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -99,6 +99,7 @@ import { ImageIcon, IncidentioIcon, InfisicalIcon, + InstantlyIcon, IntercomIcon, JinaAIIcon, JiraIcon, @@ -319,6 +320,7 @@ export const blockTypeToIconMap: Record = { imap: MailServerIcon, incidentio: IncidentioIcon, infisical: InfisicalIcon, + instantly: InstantlyIcon, intercom: IntercomIcon, intercom_v2: IntercomIcon, jina: JinaAIIcon, diff --git a/apps/docs/content/docs/en/tools/instantly.mdx b/apps/docs/content/docs/en/tools/instantly.mdx new file mode 100644 index 00000000000..354e86636ce --- /dev/null +++ b/apps/docs/content/docs/en/tools/instantly.mdx @@ -0,0 +1,522 @@ +--- +title: Instantly +description: Manage Instantly leads, campaigns, emails, and lead lists +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +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. + + + +## Tools + +### `instantly_list_leads` + +Retrieves Instantly V2 leads with search, campaign, list, and pagination filters. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `search` | string | No | Search by first name, last name, or email | +| `filter` | string | No | Instantly lead filter value, such as FILTER_VAL_CONTACTED or FILTER_VAL_ACTIVE | +| `campaign` | string | No | Campaign ID to filter leads | +| `list_id` | string | No | Lead list ID to filter leads | +| `in_campaign` | boolean | No | Whether the lead is in a campaign | +| `in_list` | boolean | No | Whether the lead is in a list | +| `ids` | array | No | Lead IDs to include | +| `items` | string | No | No description | +| `excluded_ids` | array | No | Lead IDs to exclude | +| `items` | string | No | No description | +| `contacts` | array | No | Lead email addresses to include | +| `items` | string | No | No description | +| `limit` | number | No | Number of leads to return, from 1 to 100 | +| `starting_after` | string | No | Forward pagination cursor from next_starting_after | +| `distinct_contacts` | boolean | No | Whether to return distinct contacts | +| `enrichment_status` | number | No | Enrichment status filter | +| `esg_code` | string | No | Email security gateway code filter | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `lead` | json | Lead details | +| `campaigns` | array | List of campaigns | +| `campaign` | json | Campaign details | +| `emails` | array | List of emails | +| `email` | json | Email details | +| `lead_lists` | array | List of lead lists | +| `lead_list` | json | Lead list details | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + +### `instantly_get_lead` + +Retrieves an Instantly V2 lead by ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `leadId` | string | Yes | Lead ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `lead` | json | Lead details | +| `campaigns` | array | List of campaigns | +| `campaign` | json | Campaign details | +| `emails` | array | List of emails | +| `email` | json | Email details | +| `lead_lists` | array | List of lead lists | +| `lead_list` | json | Lead list details | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + +### `instantly_create_lead` + +Creates an Instantly V2 lead in a campaign or lead list. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `campaign` | string | No | Campaign ID associated with the lead | +| `list_id` | string | No | Lead list ID associated with the lead | +| `email` | string | No | Lead email address. Required when adding to a campaign. | +| `first_name` | string | No | Lead first name | +| `last_name` | string | No | Lead last name | +| `company_name` | string | No | Lead company name | +| `job_title` | string | No | Lead job title | +| `phone` | string | No | Lead phone number | +| `website` | string | No | Lead website | +| `personalization` | string | No | Lead personalization text | +| `lt_interest_status` | number | No | Lead interest status value | +| `pl_value_lead` | string | No | Potential value of the lead | +| `assigned_to` | string | No | Organization user ID assigned to the lead | +| `skip_if_in_workspace` | boolean | No | Skip if the lead already exists in the workspace | +| `skip_if_in_campaign` | boolean | No | Skip if the lead already exists in the campaign | +| `skip_if_in_list` | boolean | No | Skip if the lead already exists in the list | +| `blocklist_id` | string | No | Blocklist ID to check for the lead | +| `verify_leads_on_import` | boolean | No | Whether to verify leads on import | +| `custom_variables` | json | No | Custom variable object with string, number, boolean, or null values | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `lead` | json | Lead details | +| `campaigns` | array | List of campaigns | +| `campaign` | json | Campaign details | +| `emails` | array | List of emails | +| `email` | json | Email details | +| `lead_lists` | array | List of lead lists | +| `lead_list` | json | Lead list details | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + +### `instantly_delete_leads` + +Deletes Instantly V2 leads in bulk from a campaign or lead list. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `campaign_id` | string | No | Campaign ID to delete leads from. Required if list_id is not provided. | +| `list_id` | string | No | Lead list ID to delete leads from. Required if campaign_id is not provided. | +| `status` | number | No | Optional lead status filter | +| `ids` | array | No | Specific lead IDs to delete | +| `items` | string | No | No description | +| `limit` | number | No | Maximum number of matching leads to delete, up to 10000 | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `count` | number | Number of leads deleted | + +### `instantly_update_lead_interest_status` + +Submits an Instantly V2 background job to update a lead interest status. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `lead_email` | string | Yes | Lead email address | +| `interest_value` | number | Yes | Interest status value. Use null in JSON/tool input to reset to Lead. | +| `campaign_id` | string | No | Campaign ID for the lead | +| `list_id` | string | No | Lead list ID for the lead | +| `ai_interest_value` | number | No | AI interest value to set for the lead | +| `disable_auto_interest` | boolean | No | Whether to disable auto interest | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Background job submission message | + +### `instantly_list_campaigns` + +Retrieves Instantly V2 campaigns with search, status, tag, and pagination filters. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | number | No | Number of campaigns to return, from 1 to 100 | +| `starting_after` | string | No | Pagination cursor from next_starting_after | +| `search` | string | No | Search by campaign name | +| `tag_ids` | string | No | Comma-separated campaign tag IDs | +| `ai_sales_agent_id` | string | No | AI Sales Agent ID filter | +| `status` | number | No | Campaign status enum value | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `lead` | json | Lead details | +| `campaigns` | array | List of campaigns | +| `campaign` | json | Campaign details | +| `emails` | array | List of emails | +| `email` | json | Email details | +| `lead_lists` | array | List of lead lists | +| `lead_list` | json | Lead list details | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + +### `instantly_create_campaign` + +Creates an Instantly V2 campaign using the documented campaign schedule schema. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | Yes | Campaign name | +| `campaign_schedule` | json | Yes | Campaign schedule object with schedules array | +| `sequences` | array | No | Campaign sequence definitions | +| `items` | object | No | No description | +| `email_list` | array | No | Sending email accounts | +| `items` | string | No | No description | +| `daily_limit` | number | No | Daily sending limit | +| `daily_max_leads` | number | No | Daily maximum new leads to contact | +| `open_tracking` | boolean | No | Whether to track opens | +| `stop_on_reply` | boolean | No | Whether to stop the campaign on reply | +| `link_tracking` | boolean | No | Whether to track links | +| `text_only` | boolean | No | Whether the campaign is text only | +| `email_gap` | number | No | Gap between emails in minutes | +| `pl_value` | number | No | Value of every positive lead | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `lead` | json | Lead details | +| `campaigns` | array | List of campaigns | +| `campaign` | json | Campaign details | +| `emails` | array | List of emails | +| `email` | json | Email details | +| `lead_lists` | array | List of lead lists | +| `lead_list` | json | Lead list details | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + +### `instantly_patch_campaign` + +Updates documented Instantly V2 campaign fields. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `campaignId` | string | Yes | Campaign ID | +| `name` | string | No | Campaign name | +| `campaign_schedule` | json | No | Campaign schedule object with schedules array | +| `sequences` | array | No | Campaign sequence definitions | +| `items` | object | No | No description | +| `email_list` | array | No | Sending email accounts | +| `items` | string | No | No description | +| `daily_limit` | number | No | Daily sending limit | +| `daily_max_leads` | number | No | Daily maximum new leads to contact | +| `open_tracking` | boolean | No | Whether to track opens | +| `stop_on_reply` | boolean | No | Whether to stop the campaign on reply | +| `link_tracking` | boolean | No | Whether to track links | +| `text_only` | boolean | No | Whether the campaign is text only | +| `email_gap` | number | No | Gap between emails in minutes | +| `pl_value` | number | No | Value of every positive lead | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `lead` | json | Lead details | +| `campaigns` | array | List of campaigns | +| `campaign` | json | Campaign details | +| `emails` | array | List of emails | +| `email` | json | Email details | +| `lead_lists` | array | List of lead lists | +| `lead_list` | json | Lead list details | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + +### `instantly_activate_campaign` + +Activates, starts, or resumes an Instantly V2 campaign. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `campaignId` | string | Yes | Campaign ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `lead` | json | Lead details | +| `campaigns` | array | List of campaigns | +| `campaign` | json | Campaign details | +| `emails` | array | List of emails | +| `email` | json | Email details | +| `lead_lists` | array | List of lead lists | +| `lead_list` | json | Lead list details | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + +### `instantly_list_emails` + +Retrieves Instantly V2 Unibox emails with search and pagination filters. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | number | No | Number of emails to return, from 1 to 100 | +| `starting_after` | string | No | Pagination cursor from next_starting_after | +| `search` | string | No | Search query, email address, or thread:<thread-id> | +| `campaign_id` | string | No | Campaign ID filter | +| `list_id` | string | No | Lead list ID filter | +| `i_status` | number | No | Email interest status filter | +| `eaccount` | string | No | Sending email account filter | +| `lead` | string | No | Lead email address filter | +| `lead_id` | string | No | Lead ID filter | +| `is_unread` | number | No | Unread status filter | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `lead` | json | Lead details | +| `campaigns` | array | List of campaigns | +| `campaign` | json | Campaign details | +| `emails` | array | List of emails | +| `email` | json | Email details | +| `lead_lists` | array | List of lead lists | +| `lead_list` | json | Lead list details | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + +### `instantly_reply_to_email` + +Sends an Instantly V2 reply to an existing Unibox email. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `eaccount` | string | Yes | Connected email account used to send the reply | +| `reply_to_uuid` | string | Yes | Email ID to reply to | +| `subject` | string | Yes | Reply subject | +| `body` | json | Yes | Reply body object with text and/or html | +| `cc_address_email_list` | string | No | Comma-separated CC email addresses | +| `bcc_address_email_list` | string | No | Comma-separated BCC email addresses | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `lead` | json | Lead details | +| `campaigns` | array | List of campaigns | +| `campaign` | json | Campaign details | +| `emails` | array | List of emails | +| `email` | json | Email details | +| `lead_lists` | array | List of lead lists | +| `lead_list` | json | Lead list details | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + +### `instantly_list_lead_lists` + +Retrieves Instantly V2 lead lists with search and pagination filters. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | number | No | Number of lead lists to return, from 1 to 100 | +| `starting_after` | string | No | Starting-after timestamp cursor | +| `has_enrichment_task` | boolean | No | Filter by enrichment task setting | +| `search` | string | No | Search query to filter lead lists by name | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `lead` | json | Lead details | +| `campaigns` | array | List of campaigns | +| `campaign` | json | Campaign details | +| `emails` | array | List of emails | +| `email` | json | Email details | +| `lead_lists` | array | List of lead lists | +| `lead_list` | json | Lead list details | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + +### `instantly_create_lead_list` + +Creates an Instantly V2 lead list. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | Yes | Lead list name | +| `has_enrichment_task` | boolean | No | Whether this list runs enrichment for every added lead | +| `owned_by` | string | No | User ID of the lead list owner | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `lead` | json | Lead details | +| `campaigns` | array | List of campaigns | +| `campaign` | json | Campaign details | +| `emails` | array | List of emails | +| `email` | json | Email details | +| `lead_lists` | array | List of lead lists | +| `lead_list` | json | Lead list details | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index f259653fdaa..4ebb74aacca 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -95,6 +95,7 @@ "imap", "incidentio", "infisical", + "instantly", "intercom", "jina", "jira", diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index ff677b09153..8db9d2f447b 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -98,6 +98,7 @@ import { ImageIcon, IncidentioIcon, InfisicalIcon, + InstantlyIcon, IntercomIcon, JinaAIIcon, JiraIcon, @@ -305,6 +306,7 @@ export const blockTypeToIconMap: Record = { imap: MailServerIcon, incidentio: IncidentioIcon, infisical: InfisicalIcon, + instantly: InstantlyIcon, intercom_v2: IntercomIcon, jina: JinaAIIcon, jira: JiraIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index bb13c23c653..6d16b77e08e 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -7028,6 +7028,178 @@ "integrationTypes": ["security"], "tags": ["secrets-management"] }, + { + "type": "instantly", + "slug": "instantly", + "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.", + "bgColor": "#FF6B35", + "iconName": "InstantlyIcon", + "docsUrl": "https://docs.sim.ai/tools/instantly", + "operations": [ + { + "name": "List Leads", + "description": "Retrieves Instantly V2 leads with search, campaign, list, and pagination filters." + }, + { + "name": "Get Lead", + "description": "Retrieves an Instantly V2 lead by ID." + }, + { + "name": "Create Lead", + "description": "Creates an Instantly V2 lead in a campaign or lead list." + }, + { + "name": "Delete Leads", + "description": "Deletes Instantly V2 leads in bulk from a campaign or lead list." + }, + { + "name": "Update Lead Interest Status", + "description": "Submits an Instantly V2 background job to update a lead interest status." + }, + { + "name": "List Campaigns", + "description": "Retrieves Instantly V2 campaigns with search, status, tag, and pagination filters." + }, + { + "name": "Create Campaign", + "description": "Creates an Instantly V2 campaign using the documented campaign schedule schema." + }, + { + "name": "Patch Campaign", + "description": "Updates documented Instantly V2 campaign fields." + }, + { + "name": "Activate Campaign", + "description": "Activates, starts, or resumes an Instantly V2 campaign." + }, + { + "name": "List Emails", + "description": "Retrieves Instantly V2 Unibox emails with search and pagination filters." + }, + { + "name": "Reply To Email", + "description": "Sends an Instantly V2 reply to an existing Unibox email." + }, + { + "name": "List Lead Lists", + "description": "Retrieves Instantly V2 lead lists with search and pagination filters." + }, + { + "name": "Create Lead List", + "description": "Creates an Instantly V2 lead list." + } + ], + "operationCount": 13, + "triggers": [ + { + "id": "instantly_webhook", + "name": "Instantly Webhook", + "description": "Trigger workflow on any Instantly webhook event" + }, + { + "id": "instantly_email_sent", + "name": "Instantly Email Sent", + "description": "Trigger when Instantly sends an email" + }, + { + "id": "instantly_email_opened", + "name": "Instantly Email Opened", + "description": "Trigger when a lead opens an Instantly email" + }, + { + "id": "instantly_reply_received", + "name": "Instantly Reply Received", + "description": "Trigger when a lead replies to an Instantly email" + }, + { + "id": "instantly_auto_reply_received", + "name": "Instantly Auto Reply Received", + "description": "Trigger when Instantly receives an auto-reply from a lead" + }, + { + "id": "instantly_link_clicked", + "name": "Instantly Link Clicked", + "description": "Trigger when a lead clicks a tracked Instantly link" + }, + { + "id": "instantly_email_bounced", + "name": "Instantly Email Bounced", + "description": "Trigger when an Instantly email bounces" + }, + { + "id": "instantly_lead_unsubscribed", + "name": "Instantly Lead Unsubscribed", + "description": "Trigger when an Instantly lead unsubscribes" + }, + { + "id": "instantly_account_error", + "name": "Instantly Account Error", + "description": "Trigger when Instantly reports an account-level error" + }, + { + "id": "instantly_campaign_completed", + "name": "Instantly Campaign Completed", + "description": "Trigger when an Instantly campaign completes" + }, + { + "id": "instantly_lead_neutral", + "name": "Instantly Lead Neutral", + "description": "Trigger when an Instantly lead is marked neutral" + }, + { + "id": "instantly_lead_interested", + "name": "Instantly Lead Interested", + "description": "Trigger when an Instantly lead is marked interested" + }, + { + "id": "instantly_lead_not_interested", + "name": "Instantly Lead Not Interested", + "description": "Trigger when an Instantly lead is marked not interested" + }, + { + "id": "instantly_lead_meeting_booked", + "name": "Instantly Lead Meeting Booked", + "description": "Trigger when an Instantly lead books a meeting" + }, + { + "id": "instantly_lead_meeting_completed", + "name": "Instantly Lead Meeting Completed", + "description": "Trigger when an Instantly lead completes a meeting" + }, + { + "id": "instantly_lead_closed", + "name": "Instantly Lead Closed", + "description": "Trigger when an Instantly lead is marked closed" + }, + { + "id": "instantly_lead_out_of_office", + "name": "Instantly Lead Out Of Office", + "description": "Trigger when an Instantly lead is out of office" + }, + { + "id": "instantly_lead_wrong_person", + "name": "Instantly Lead Wrong Person", + "description": "Trigger when an Instantly lead is marked wrong person" + }, + { + "id": "instantly_lead_no_show", + "name": "Instantly Lead No Show", + "description": "Trigger when an Instantly lead is marked no show" + }, + { + "id": "instantly_supersearch_enrichment_completed", + "name": "Instantly Supersearch Enrichment Completed", + "description": "Trigger when Instantly completes a Supersearch enrichment" + } + ], + "triggerCount": 20, + "authType": "api-key", + "category": "tools", + "integrationTypes": ["email", "developer-tools", "sales"], + "tags": ["sales-engagement", "email-marketing", "automation"] + }, { "type": "intercom_v2", "slug": "intercom", diff --git a/apps/sim/blocks/blocks/instantly.ts b/apps/sim/blocks/blocks/instantly.ts new file mode 100644 index 00000000000..cde31692378 --- /dev/null +++ b/apps/sim/blocks/blocks/instantly.ts @@ -0,0 +1,849 @@ +import { InstantlyIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +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_CREATE_OPERATIONS = ['create_lead'] 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 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 +const LEAD_LIST_CREATE_OPERATIONS = ['create_lead_list'] as const +const PAGINATED_OPERATIONS = [ + 'list_leads', + 'list_campaigns', + 'list_emails', + 'list_lead_lists', +] as const +const INSTANTLY_TRIGGER_IDS = [ + 'instantly_webhook', + 'instantly_email_sent', + 'instantly_email_opened', + 'instantly_reply_received', + 'instantly_auto_reply_received', + 'instantly_link_clicked', + 'instantly_email_bounced', + 'instantly_lead_unsubscribed', + 'instantly_account_error', + 'instantly_campaign_completed', + 'instantly_lead_neutral', + 'instantly_lead_interested', + 'instantly_lead_not_interested', + 'instantly_lead_meeting_booked', + 'instantly_lead_meeting_completed', + 'instantly_lead_closed', + 'instantly_lead_out_of_office', + 'instantly_lead_wrong_person', + 'instantly_lead_no_show', + 'instantly_supersearch_enrichment_completed', +] as const + +export const InstantlyBlock: BlockConfig = { + type: 'instantly', + 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.', + docsLink: 'https://docs.sim.ai/tools/instantly', + category: 'tools', + integrationType: IntegrationType.Email, + tags: ['sales-engagement', 'email-marketing', 'automation'], + bgColor: '#FF6B35', + icon: InstantlyIcon, + authMode: AuthMode.ApiKey, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'List Leads', id: 'list_leads' }, + { label: 'Get Lead', id: 'get_lead' }, + { label: 'Create Lead', id: 'create_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: 'List Emails', id: 'list_emails' }, + { label: 'Reply To Email', id: 'reply_to_email' }, + { label: 'List Lead Lists', id: 'list_lead_lists' }, + { label: 'Create Lead List', id: 'create_lead_list' }, + ], + value: () => 'list_leads', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Instantly API key', + password: true, + required: true, + }, + { + id: 'leadId', + title: 'Lead ID', + type: 'short-input', + placeholder: '019e3bd1-b5d9-7b0a-9823-d5382bc9d72b', + required: { field: 'operation', value: [...LEAD_ID_OPERATIONS] }, + condition: { field: 'operation', value: [...LEAD_ID_OPERATIONS] }, + }, + { + id: 'leadDestination', + title: 'Add Lead To', + type: 'dropdown', + options: [ + { label: 'Campaign', id: 'campaign' }, + { label: 'Lead List', id: 'list' }, + ], + value: () => 'campaign', + required: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + }, + { + id: 'leadDestinationId', + title: 'Campaign or Lead List ID', + type: 'short-input', + placeholder: 'Destination UUID', + required: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + }, + { + id: 'email', + title: 'Lead Email', + type: 'short-input', + placeholder: 'jane@example.com', + required: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + }, + { + id: 'firstName', + title: 'First Name', + type: 'short-input', + placeholder: 'Jane', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + }, + { + id: 'lastName', + title: 'Last Name', + type: 'short-input', + placeholder: 'Doe', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + }, + { + id: 'companyName', + title: 'Company Name', + type: 'short-input', + placeholder: 'Acme Inc.', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + }, + { + id: 'jobTitle', + title: 'Job Title', + type: 'short-input', + placeholder: 'Head of Growth', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'phone', + title: 'Phone', + type: 'short-input', + placeholder: '+1234567890', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'website', + title: 'Website', + type: 'short-input', + placeholder: 'https://example.com', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'personalization', + title: 'Personalization', + type: 'long-input', + placeholder: 'Personalized opening line', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'customVariables', + title: 'Custom Variables', + type: 'long-input', + placeholder: '{"past_customer": true, "industry": "SaaS"}', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON object of Instantly custom variables. Values must be strings, numbers, booleans, or null. Return ONLY the JSON object.', + generationType: 'json-object', + }, + mode: 'advanced', + }, + { + id: 'skipIfInWorkspace', + title: 'Skip If In Workspace', + type: 'dropdown', + options: [ + { label: 'Unspecified', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'skipIfInCampaign', + title: 'Skip If In Campaign', + type: 'dropdown', + options: [ + { label: 'Unspecified', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'skipIfInList', + title: 'Skip If In List', + type: 'dropdown', + options: [ + { label: 'Unspecified', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'search', + title: 'Search', + type: 'short-input', + placeholder: 'Search term', + condition: { + field: 'operation', + value: [ + ...LEAD_LIST_OPERATIONS, + ...CAMPAIGN_LIST_OPERATIONS, + ...EMAIL_LIST_OPERATIONS, + ...LEAD_LIST_LIST_OPERATIONS, + ], + }, + }, + { + id: 'leadFilter', + title: 'Lead Filter', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'Contacted', id: 'FILTER_VAL_CONTACTED' }, + { label: 'Not Contacted', id: 'FILTER_VAL_NOT_CONTACTED' }, + { label: 'Completed', id: 'FILTER_VAL_COMPLETED' }, + { label: 'Unsubscribed', id: 'FILTER_VAL_UNSUBSCRIBED' }, + { label: 'Active', id: 'FILTER_VAL_ACTIVE' }, + { label: 'Interested', id: 'FILTER_LEAD_INTERESTED' }, + { label: 'Not Interested', id: 'FILTER_LEAD_NOT_INTERESTED' }, + { label: 'Meeting Booked', id: 'FILTER_LEAD_MEETING_BOOKED' }, + { label: 'Replied', id: 'FILTER_VAL_REPLIED' }, + { label: 'Link Clicked', id: 'FILTER_VAL_LINK_CLICKED' }, + ], + value: () => '', + condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'campaignId', + title: 'Campaign ID', + type: 'short-input', + placeholder: 'Campaign UUID', + required: { field: 'operation', value: [...CAMPAIGN_ID_OPERATIONS] }, + condition: { + field: 'operation', + value: [ + ...LEAD_LIST_OPERATIONS, + ...LEAD_INTEREST_OPERATIONS, + ...CAMPAIGN_ID_OPERATIONS, + ...EMAIL_LIST_OPERATIONS, + ], + }, + }, + { + id: 'listId', + title: 'Lead List ID', + type: 'short-input', + placeholder: 'Lead list UUID', + condition: { + field: 'operation', + value: [...LEAD_LIST_OPERATIONS, ...LEAD_INTEREST_OPERATIONS, ...EMAIL_LIST_OPERATIONS], + }, + }, + { + id: 'leadIds', + title: 'Lead IDs', + type: 'long-input', + placeholder: 'lead-id-1, lead-id-2', + condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'contacts', + title: 'Contacts', + type: 'long-input', + placeholder: 'jane@example.com, john@example.com', + condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'inCampaign', + title: 'In Campaign', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'inList', + title: 'In List', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'deleteSource', + title: 'Delete From', + type: 'dropdown', + options: [ + { label: 'Campaign', id: 'campaign' }, + { label: 'Lead List', id: 'list' }, + ], + value: () => 'campaign', + required: { field: 'operation', value: [...LEAD_DELETE_OPERATIONS] }, + condition: { field: 'operation', value: [...LEAD_DELETE_OPERATIONS] }, + }, + { + id: 'deleteSourceId', + title: 'Campaign or Lead List ID', + type: 'short-input', + placeholder: 'Source UUID', + required: { field: 'operation', value: [...LEAD_DELETE_OPERATIONS] }, + condition: { field: 'operation', value: [...LEAD_DELETE_OPERATIONS] }, + }, + { + id: 'deleteStatus', + title: 'Delete Status Filter', + type: 'short-input', + placeholder: '3', + condition: { field: 'operation', value: [...LEAD_DELETE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'deleteLeadIds', + title: 'Delete Lead IDs', + type: 'long-input', + placeholder: 'lead-id-1, lead-id-2', + condition: { field: 'operation', value: [...LEAD_DELETE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'deleteLimit', + title: 'Delete Limit', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: [...LEAD_DELETE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'leadEmail', + title: 'Lead Email', + type: 'short-input', + placeholder: 'jane@example.com', + required: { field: 'operation', value: [...LEAD_INTEREST_OPERATIONS] }, + condition: { field: 'operation', value: [...LEAD_INTEREST_OPERATIONS] }, + }, + { + id: 'interestValue', + title: 'Interest Value', + type: 'short-input', + placeholder: '1', + required: { field: 'operation', value: [...LEAD_INTEREST_OPERATIONS] }, + condition: { field: 'operation', value: [...LEAD_INTEREST_OPERATIONS] }, + }, + { + id: 'disableAutoInterest', + title: 'Disable Auto Interest', + type: 'dropdown', + options: [ + { label: 'Unspecified', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...LEAD_INTEREST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'campaignName', + title: 'Campaign Name', + type: 'short-input', + placeholder: 'My First Campaign', + required: { field: 'operation', value: 'create_campaign' }, + condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, + }, + { + id: 'campaignSchedule', + title: 'Campaign Schedule', + type: 'long-input', + placeholder: + '{"schedules":[{"name":"Weekdays","timing":{"from":"09:00","to":"17:00"},"days":{"1":true,"2":true,"3":true,"4":true,"5":true},"timezone":"America/Los_Angeles"}]}', + required: { field: 'operation', value: 'create_campaign' }, + condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, + wandConfig: { + enabled: true, + prompt: + 'Generate an Instantly API V2 campaign_schedule JSON object with schedules containing name, timing.from, timing.to, days, and timezone. Return ONLY the JSON object.', + generationType: 'json-object', + }, + }, + { + id: 'sequences', + title: 'Sequences', + type: 'long-input', + placeholder: + '[{"steps":[{"type":"email","delay":2,"variants":[{"subject":"Hello {{firstName}}","body":"Hey {{firstName}},\\n\\nI hope you are doing well."}]}]}]', + condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, + wandConfig: { + enabled: true, + prompt: + 'Generate an Instantly API V2 sequences JSON array. Use one sequence with steps; each step must have type "email", delay, and variants with subject and body. Return ONLY the JSON array.', + generationType: 'json-object', + }, + mode: 'advanced', + }, + { + id: 'emailList', + title: 'Sending Accounts', + type: 'long-input', + placeholder: 'sender@example.com, sender2@example.com', + condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'dailyLimit', + title: 'Daily Limit', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'dailyMaxLeads', + title: 'Daily Max Leads', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'openTracking', + title: 'Open Tracking', + type: 'dropdown', + options: [ + { label: 'Unspecified', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'stopOnReply', + title: 'Stop On Reply', + type: 'dropdown', + options: [ + { label: 'Unspecified', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'tagIds', + title: 'Tag IDs', + type: 'short-input', + placeholder: 'id1,id2', + condition: { field: 'operation', value: [...CAMPAIGN_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'campaignStatus', + title: 'Campaign Status', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: [...CAMPAIGN_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'emailAccount', + title: 'Email Account', + type: 'short-input', + placeholder: 'sender@example.com', + condition: { + field: 'operation', + value: [...EMAIL_LIST_OPERATIONS, ...EMAIL_REPLY_OPERATIONS], + }, + required: { field: 'operation', value: [...EMAIL_REPLY_OPERATIONS] }, + }, + { + id: 'emailSearch', + title: 'Email Search', + type: 'short-input', + placeholder: 'lead@example.com or thread:uuid', + condition: { field: 'operation', value: [...EMAIL_LIST_OPERATIONS] }, + }, + { + id: 'replyToUuid', + title: 'Reply To Email ID', + type: 'short-input', + placeholder: 'Email UUID', + required: { field: 'operation', value: [...EMAIL_REPLY_OPERATIONS] }, + condition: { field: 'operation', value: [...EMAIL_REPLY_OPERATIONS] }, + }, + { + id: 'subject', + title: 'Subject', + type: 'short-input', + placeholder: 'Re: Your inquiry', + required: { field: 'operation', value: [...EMAIL_REPLY_OPERATIONS] }, + condition: { field: 'operation', value: [...EMAIL_REPLY_OPERATIONS] }, + }, + { + id: 'bodyText', + title: 'Body Text', + type: 'long-input', + placeholder: 'Plain text reply body', + required: { field: 'operation', value: [...EMAIL_REPLY_OPERATIONS] }, + condition: { field: 'operation', value: [...EMAIL_REPLY_OPERATIONS] }, + }, + { + id: 'bodyHtml', + title: 'Body HTML', + type: 'long-input', + placeholder: '

HTML reply body

', + condition: { field: 'operation', value: [...EMAIL_REPLY_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'leadListName', + title: 'Lead List Name', + type: 'short-input', + placeholder: 'My Lead List', + required: { field: 'operation', value: [...LEAD_LIST_CREATE_OPERATIONS] }, + condition: { field: 'operation', value: [...LEAD_LIST_CREATE_OPERATIONS] }, + }, + { + id: 'hasEnrichmentTask', + title: 'Has Enrichment Task', + type: 'dropdown', + options: [ + { label: 'Unspecified', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { + field: 'operation', + value: [...LEAD_LIST_LIST_OPERATIONS, ...LEAD_LIST_CREATE_OPERATIONS], + }, + mode: 'advanced', + }, + { + id: 'ownedBy', + title: 'Owned By', + type: 'short-input', + placeholder: 'User UUID', + condition: { field: 'operation', value: [...LEAD_LIST_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '10', + condition: { field: 'operation', value: [...PAGINATED_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'startingAfter', + title: 'Starting After', + type: 'short-input', + placeholder: 'Cursor from next_starting_after', + condition: { field: 'operation', value: [...PAGINATED_OPERATIONS] }, + mode: 'advanced', + }, + ...INSTANTLY_TRIGGER_IDS.flatMap((triggerId) => getTrigger(triggerId).subBlocks), + ], + tools: { + access: [ + 'instantly_list_leads', + 'instantly_get_lead', + 'instantly_create_lead', + 'instantly_delete_leads', + 'instantly_update_lead_interest_status', + 'instantly_list_campaigns', + 'instantly_create_campaign', + 'instantly_patch_campaign', + 'instantly_activate_campaign', + 'instantly_list_emails', + 'instantly_reply_to_email', + 'instantly_list_lead_lists', + 'instantly_create_lead_list', + ], + config: { + tool: (params) => `instantly_${params.operation}`, + params: (params) => ({ + campaign: + params.leadDestination === 'campaign' + ? emptyToUndefined(params.leadDestinationId) + : undefined, + list_id: + params.operation === 'delete_leads' && params.deleteSource === 'list' + ? emptyToUndefined(params.deleteSourceId) + : params.leadDestination === 'list' + ? emptyToUndefined(params.leadDestinationId) + : emptyToUndefined(params.listId), + campaign_id: + params.operation === 'delete_leads' + ? params.deleteSource === 'campaign' + ? emptyToUndefined(params.deleteSourceId) + : undefined + : emptyToUndefined(params.campaignId), + leadId: params.leadId, + email: params.email, + first_name: params.firstName, + last_name: params.lastName, + company_name: params.companyName, + job_title: params.jobTitle, + phone: params.phone, + website: params.website, + personalization: params.personalization, + custom_variables: parseJsonObject(params.customVariables), + skip_if_in_workspace: toBooleanParam(params.skipIfInWorkspace), + skip_if_in_campaign: toBooleanParam(params.skipIfInCampaign), + skip_if_in_list: toBooleanParam(params.skipIfInList), + filter: emptyToUndefined(params.leadFilter), + ids: + params.operation === 'delete_leads' + ? parseStringList(params.deleteLeadIds) + : parseStringList(params.leadIds), + contacts: parseStringList(params.contacts), + in_campaign: toBooleanParam(params.inCampaign), + in_list: toBooleanParam(params.inList), + status: + params.operation === 'delete_leads' + ? toNumberParam(params.deleteStatus) + : toNumberParam(params.campaignStatus), + limit: + params.operation === 'delete_leads' + ? toNumberParam(params.deleteLimit) + : toNumberParam(params.limit), + starting_after: params.startingAfter, + lead_email: params.leadEmail, + interest_value: toNumberParam(params.interestValue), + disable_auto_interest: toBooleanParam(params.disableAutoInterest), + name: + params.operation === 'create_lead_list' + ? params.leadListName + : emptyToUndefined(params.campaignName), + campaign_schedule: parseJsonObject(params.campaignSchedule), + sequences: parseJsonArray(params.sequences), + email_list: parseStringList(params.emailList), + daily_limit: toNumberParam(params.dailyLimit), + daily_max_leads: toNumberParam(params.dailyMaxLeads), + open_tracking: toBooleanParam(params.openTracking), + stop_on_reply: toBooleanParam(params.stopOnReply), + tag_ids: emptyToUndefined(params.tagIds), + search: + params.operation === 'list_emails' + ? emptyToUndefined(params.emailSearch) + : emptyToUndefined(params.search), + eaccount: params.emailAccount, + reply_to_uuid: params.replyToUuid, + subject: params.subject, + body: { + text: params.bodyText, + html: emptyToUndefined(params.bodyHtml), + }, + has_enrichment_task: toBooleanParam(params.hasEnrichmentTask), + owned_by: emptyToUndefined(params.ownedBy), + }), + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Instantly API key' }, + leadId: { type: 'string', description: 'Lead ID' }, + leadDestination: { type: 'string', description: 'Create lead destination type' }, + leadDestinationId: { type: 'string', description: 'Create lead destination ID' }, + email: { type: 'string', description: 'Lead email' }, + firstName: { type: 'string', description: 'Lead first name' }, + lastName: { type: 'string', description: 'Lead last name' }, + companyName: { type: 'string', description: 'Company name' }, + search: { type: 'string', description: 'Search query' }, + campaignId: { type: 'string', description: 'Campaign ID' }, + listId: { type: 'string', description: 'Lead list ID' }, + deleteSource: { type: 'string', description: 'Delete source type' }, + deleteSourceId: { type: 'string', description: 'Delete source ID' }, + leadEmail: { type: 'string', description: 'Lead email for interest update' }, + interestValue: { type: 'number', description: 'Interest status value' }, + campaignName: { type: 'string', description: 'Campaign name' }, + campaignSchedule: { type: 'json', description: 'Campaign schedule object' }, + sequences: { type: 'array', description: 'Campaign sequences' }, + emailAccount: { type: 'string', description: 'Email account' }, + emailSearch: { type: 'string', description: 'Email search query' }, + replyToUuid: { type: 'string', description: 'Email ID to reply to' }, + subject: { type: 'string', description: 'Reply subject' }, + bodyText: { type: 'string', description: 'Reply body text' }, + leadListName: { type: 'string', description: 'Lead list name' }, + limit: { type: 'number', description: 'Page size' }, + startingAfter: { type: 'string', description: 'Pagination cursor' }, + }, + outputs: { + leads: { type: 'array', description: 'List of leads' }, + lead: { type: 'json', description: 'Lead details' }, + campaigns: { type: 'array', description: 'List of campaigns' }, + campaign: { type: 'json', description: 'Campaign details' }, + emails: { type: 'array', description: 'List of emails' }, + email: { type: 'json', description: 'Email details' }, + lead_lists: { type: 'array', description: 'List of lead lists' }, + lead_list: { type: 'json', description: 'Lead list details' }, + count: { type: 'number', description: 'Returned or affected record count' }, + next_starting_after: { type: 'string', description: 'Cursor for the next page' }, + id: { type: 'string', description: 'Record ID' }, + name: { type: 'string', description: 'Record name' }, + email_address: { type: 'string', description: 'Lead email address' }, + first_name: { type: 'string', description: 'Lead first name' }, + last_name: { type: 'string', description: 'Lead last name' }, + status: { type: 'number', description: 'Lead or campaign status' }, + subject: { type: 'string', description: 'Email subject' }, + thread_id: { type: 'string', description: 'Email thread ID' }, + message: { type: 'string', description: 'Operation message' }, + }, + triggers: { + enabled: true, + available: [ + 'instantly_webhook', + 'instantly_email_sent', + 'instantly_email_opened', + 'instantly_reply_received', + 'instantly_auto_reply_received', + 'instantly_link_clicked', + 'instantly_email_bounced', + 'instantly_lead_unsubscribed', + 'instantly_account_error', + 'instantly_campaign_completed', + 'instantly_lead_neutral', + 'instantly_lead_interested', + 'instantly_lead_not_interested', + 'instantly_lead_meeting_booked', + 'instantly_lead_meeting_completed', + 'instantly_lead_closed', + 'instantly_lead_out_of_office', + 'instantly_lead_wrong_person', + 'instantly_lead_no_show', + 'instantly_supersearch_enrichment_completed', + ], + }, +} + +function parseStringList(value: unknown): string[] | undefined { + if (Array.isArray(value)) { + const strings = value.filter((item): item is string => typeof item === 'string' && item !== '') + return strings.length > 0 ? strings : undefined + } + + if (typeof value !== 'string' || value.trim() === '') return undefined + + const strings = value + .split(/[\s,]+/) + .map((item) => item.trim()) + .filter(Boolean) + + return strings.length > 0 ? strings : undefined +} + +function parseJsonObject(value: unknown): Record | undefined { + if (isPlainObject(value)) return value + if (typeof value !== 'string' || value.trim() === '') return undefined + + try { + const parsed: unknown = JSON.parse(value) + return isPlainObject(parsed) ? parsed : undefined + } catch { + return undefined + } +} + +function parseJsonArray(value: unknown): unknown[] | undefined { + if (Array.isArray(value)) return value + if (typeof value !== 'string' || value.trim() === '') return undefined + + try { + const parsed: unknown = JSON.parse(value) + return Array.isArray(parsed) ? parsed : undefined + } catch { + return undefined + } +} + +function toNumberParam(value: unknown): number | undefined { + if (typeof value === 'number') return Number.isFinite(value) ? value : undefined + if (typeof value !== 'string' || value.trim() === '') return undefined + + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined +} + +function toBooleanParam(value: unknown): boolean | undefined { + if (typeof value === 'boolean') return value + if (typeof value !== 'string' || value.trim() === '') return undefined + if (value === 'true') return true + if (value === 'false') return false + return undefined +} + +function emptyToUndefined(value: unknown): unknown { + return value === '' ? undefined : value +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 5beb6e12088..637c1ce5926 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -103,6 +103,7 @@ import { ImapBlock } from '@/blocks/blocks/imap' import { IncidentioBlock } from '@/blocks/blocks/incidentio' import { InfisicalBlock } from '@/blocks/blocks/infisical' import { InputTriggerBlock } from '@/blocks/blocks/input_trigger' +import { InstantlyBlock } from '@/blocks/blocks/instantly' import { IntercomBlock, IntercomV2Block } from '@/blocks/blocks/intercom' import { JinaBlock } from '@/blocks/blocks/jina' import { JiraBlock } from '@/blocks/blocks/jira' @@ -366,6 +367,7 @@ export const registry: Record = { input_trigger: InputTriggerBlock, intercom: IntercomBlock, intercom_v2: IntercomV2Block, + instantly: InstantlyBlock, jina: JinaBlock, jira: JiraBlock, jira_service_management: JiraServiceManagementBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 70f7c4df850..a35727905dd 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -430,6 +430,18 @@ export function MailIcon(props: SVGProps) { ) } +export function InstantlyIcon(props: SVGProps) { + return ( + + + + + ) +} + export function EmailBisonIcon(props: SVGProps) { return ( diff --git a/apps/sim/lib/webhooks/providers/instantly.ts b/apps/sim/lib/webhooks/providers/instantly.ts new file mode 100644 index 00000000000..a68b4e9d3e2 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/instantly.ts @@ -0,0 +1,259 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { generateShortId } from '@sim/utils/id' +import { NextResponse } from 'next/server' +import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import type { + AuthContext, + DeleteSubscriptionContext, + EventMatchContext, + FormatInputContext, + FormatInputResult, + SubscriptionContext, + SubscriptionResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' +import { verifyTokenAuth } from '@/lib/webhooks/providers/utils' +import { instantlyUrl } from '@/tools/instantly/utils' + +const logger = createLogger('WebhookProvider:Instantly') +const SIM_WEBHOOK_TOKEN_HEADER = 'x-sim-webhook-token' + +export const instantlyHandler: WebhookProviderHandler = { + verifyAuth({ request, requestId, providerConfig }: AuthContext): NextResponse | null { + const secretToken = providerConfig.secretToken as string | undefined + if (!secretToken) return null + + if (!verifyTokenAuth(request, secretToken, SIM_WEBHOOK_TOKEN_HEADER)) { + logger.warn(`[${requestId}] Unauthorized Instantly webhook request`) + return new NextResponse('Unauthorized', { status: 401 }) + } + + return null + }, + + async matchEvent({ body, providerConfig, requestId }: EventMatchContext): Promise { + const triggerId = providerConfig.triggerId as string | undefined + if (!triggerId) return true + + if (!isRecord(body)) { + logger.warn(`[${requestId}] Instantly webhook payload was not an object`) + return false + } + + const { isInstantlyEventMatch } = await import('@/triggers/instantly/utils') + if (!isInstantlyEventMatch(triggerId, body)) { + logger.info(`[${requestId}] Instantly event did not match trigger`, { + triggerId, + eventType: body.event_type, + }) + return false + } + + return true + }, + + async formatInput({ body }: FormatInputContext): Promise { + const payload = isRecord(body) ? body : {} + + return { + input: { + timestamp: toStringOrNull(payload.timestamp), + eventType: toStringOrNull(payload.event_type), + workspace: toStringOrNull(payload.workspace), + campaignId: toStringOrNull(payload.campaign_id), + campaignName: toStringOrNull(payload.campaign_name), + leadEmail: toStringOrNull(payload.lead_email), + emailAccount: toStringOrNull(payload.email_account), + uniboxUrl: toStringOrNull(payload.unibox_url), + step: toNumberOrNull(payload.step), + variant: toNumberOrNull(payload.variant), + isFirst: toBooleanOrNull(payload.is_first), + emailId: toStringOrNull(payload.email_id), + emailSubject: toStringOrNull(payload.email_subject), + emailText: toStringOrNull(payload.email_text), + emailHtml: toStringOrNull(payload.email_html), + replyTextSnippet: toStringOrNull(payload.reply_text_snippet), + replySubject: toStringOrNull(payload.reply_subject), + replyText: toStringOrNull(payload.reply_text), + replyHtml: toStringOrNull(payload.reply_html), + payload, + }, + } + }, + + async createSubscription(ctx: SubscriptionContext): Promise { + const { webhook, requestId } = ctx + const providerConfig = getProviderConfig(webhook) + const apiKey = providerConfig.apiKey as string | undefined + const triggerId = providerConfig.triggerId as string | undefined + const campaignId = providerConfig.triggerCampaignId as string | undefined + + if (!apiKey?.trim()) { + throw new Error('Instantly API Key is required.') + } + + if (!triggerId) { + throw new Error('Instantly trigger ID is required.') + } + + const { getInstantlySubscriptionEventTypeForTrigger } = await import( + '@/triggers/instantly/utils' + ) + const eventType = getInstantlySubscriptionEventTypeForTrigger(triggerId) + if (!eventType) { + throw new Error(`Unknown Instantly trigger type: ${triggerId}`) + } + + const secretToken = + typeof providerConfig.secretToken === 'string' && providerConfig.secretToken.length > 0 + ? providerConfig.secretToken + : generateShortId(32) + + const requestBody: Record = { + name: `Sim - ${triggerId.replace(/^instantly_/, '').replace(/_/g, ' ')}`, + target_hook_url: getNotificationUrl(webhook), + event_type: eventType, + headers: { + 'X-Sim-Webhook-Token': secretToken, + }, + } + + if (campaignId?.trim()) { + requestBody.campaign = campaignId.trim() + } + + logger.info(`[${requestId}] Creating Instantly webhook`, { + triggerId, + eventType, + hasCampaignId: Boolean(campaignId), + webhookId: webhook.id, + }) + + const response = await fetch(instantlyUrl('/api/v2/webhooks'), { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + const responseBody = await parseJsonResponse(response) + if (!response.ok) { + const message = extractInstantlyError(responseBody) + logger.error(`[${requestId}] Failed to create Instantly webhook`, { + status: response.status, + message, + response: responseBody, + }) + + if (response.status === 401 || response.status === 403) { + throw new Error('Invalid Instantly API Key or missing webhook permissions.') + } + + if (response.status === 402) { + throw new Error('Instantly webhook creation requires an active paid plan.') + } + + throw new Error( + message ? `Instantly error: ${message}` : 'Failed to create Instantly webhook' + ) + } + + const externalId = responseBody?.id + if (typeof externalId !== 'string' || externalId.length === 0) { + throw new Error('Instantly webhook was created but the API response did not include an ID.') + } + + logger.info(`[${requestId}] Successfully created Instantly webhook`, { + externalId, + webhookId: webhook.id, + }) + + return { providerConfigUpdates: { externalId, secretToken } } + }, + + async deleteSubscription(ctx: DeleteSubscriptionContext): Promise { + const { webhook, requestId } = ctx + + try { + const providerConfig = getProviderConfig(webhook) + const apiKey = providerConfig.apiKey as string | undefined + const externalId = providerConfig.externalId as string | undefined + + if (!apiKey?.trim() || !externalId?.trim()) { + logger.warn(`[${requestId}] Missing Instantly webhook cleanup configuration`, { + webhookId: webhook.id, + hasApiKey: Boolean(apiKey), + hasExternalId: Boolean(externalId), + }) + if (ctx.strict) throw new Error('Missing Instantly webhook cleanup configuration') + return + } + + const response = await fetch( + instantlyUrl(`/api/v2/webhooks/${encodeURIComponent(externalId)}`), + { + method: 'DELETE', + headers: { + Authorization: `Bearer ${apiKey}`, + }, + } + ) + + if (!response.ok && response.status !== 404) { + const responseBody = await parseJsonResponse(response) + logger.warn(`[${requestId}] Failed to delete Instantly webhook`, { + status: response.status, + response: responseBody, + }) + if (ctx.strict) throw new Error(`Failed to delete Instantly webhook: ${response.status}`) + return + } + + await response.body?.cancel() + logger.info(`[${requestId}] Successfully deleted Instantly webhook`, { + externalId, + webhookId: webhook.id, + }) + } catch (error) { + logger.warn(`[${requestId}] Error deleting Instantly webhook`, { + message: toError(error).message, + }) + if (ctx.strict) throw error + } + }, +} + +async function parseJsonResponse(response: Response): Promise | null> { + try { + const body: unknown = await response.json() + return isRecord(body) ? body : null + } catch { + return null + } +} + +function extractInstantlyError(body: Record | null): string | null { + if (!body) return null + if (typeof body.message === 'string') return body.message + if (typeof body.error === 'string') return body.error + return null +} + +function toStringOrNull(value: unknown): string | null { + return typeof value === 'string' ? value : null +} + +function toNumberOrNull(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? value : null +} + +function toBooleanOrNull(value: unknown): boolean | null { + return typeof value === 'boolean' ? value : null +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 204be8f8a4b..44b3a0b5009 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -19,6 +19,7 @@ import { googleFormsHandler } from '@/lib/webhooks/providers/google-forms' import { grainHandler } from '@/lib/webhooks/providers/grain' import { greenhouseHandler } from '@/lib/webhooks/providers/greenhouse' import { imapHandler } from '@/lib/webhooks/providers/imap' +import { instantlyHandler } from '@/lib/webhooks/providers/instantly' import { intercomHandler } from '@/lib/webhooks/providers/intercom' import { jiraHandler } from '@/lib/webhooks/providers/jira' import { jsmHandler } from '@/lib/webhooks/providers/jsm' @@ -69,6 +70,7 @@ const PROVIDER_HANDLERS: Record = { greenhouse: greenhouseHandler, imap: imapHandler, intercom: intercomHandler, + instantly: instantlyHandler, jira: jiraHandler, jsm: jsmHandler, lemlist: lemlistHandler, diff --git a/apps/sim/tools/instantly/activate_campaign.ts b/apps/sim/tools/instantly/activate_campaign.ts new file mode 100644 index 00000000000..718af46d661 --- /dev/null +++ b/apps/sim/tools/instantly/activate_campaign.ts @@ -0,0 +1,51 @@ +import type { + InstantlyActivateCampaignParams, + InstantlyCampaignResponse, +} from '@/tools/instantly/types' +import { + campaignOutputs, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + mapCampaign, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const activateCampaignTool: ToolConfig< + InstantlyActivateCampaignParams, + InstantlyCampaignResponse +> = { + id: 'instantly_activate_campaign', + name: 'Instantly Activate Campaign', + description: 'Activates, starts, or resumes 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}/activate`), + method: 'POST', + headers: instantlyHeaders, + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + const campaign = mapCampaign(data) + + return { + success: true, + output: { + campaign, + id: campaign.id, + name: campaign.name, + status: campaign.status, + }, + } + }, + outputs: campaignOutputs, +} diff --git a/apps/sim/tools/instantly/create_campaign.ts b/apps/sim/tools/instantly/create_campaign.ts new file mode 100644 index 00000000000..094dd522325 --- /dev/null +++ b/apps/sim/tools/instantly/create_campaign.ts @@ -0,0 +1,150 @@ +import type { + InstantlyCampaignResponse, + InstantlyCreateCampaignParams, +} from '@/tools/instantly/types' +import { + campaignOutputs, + compactBody, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + mapCampaign, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const createCampaignTool: ToolConfig< + InstantlyCreateCampaignParams, + InstantlyCampaignResponse +> = { + id: 'instantly_create_campaign', + name: 'Instantly Create Campaign', + description: 'Creates an Instantly V2 campaign using the documented campaign schedule schema.', + version: '1.0.0', + params: { + ...instantlyBaseParamFields, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Campaign name', + }, + campaign_schedule: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Campaign schedule object with schedules array', + }, + sequences: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Campaign sequence definitions', + items: { type: 'object', description: 'Sequence object' }, + }, + email_list: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Sending email accounts', + items: { type: 'string', description: 'Email address' }, + }, + daily_limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Daily sending limit', + }, + daily_max_leads: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Daily maximum new leads to contact', + }, + open_tracking: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to track opens', + }, + stop_on_reply: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to stop the campaign on reply', + }, + link_tracking: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to track links', + }, + text_only: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the campaign is text only', + }, + email_gap: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Gap between emails in minutes', + }, + pl_value: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Value of every positive lead', + }, + }, + request: { + url: () => instantlyUrl('/api/v2/campaigns'), + method: 'POST', + headers: instantlyHeaders, + body: (params) => + compactBody({ + name: params.name, + campaign_schedule: params.campaign_schedule, + sequences: params.sequences, + pl_value: params.pl_value, + is_evergreen: params.is_evergreen, + email_gap: params.email_gap, + random_wait_max: params.random_wait_max, + text_only: params.text_only, + first_email_text_only: params.first_email_text_only, + email_list: params.email_list, + daily_limit: params.daily_limit, + stop_on_reply: params.stop_on_reply, + email_tag_list: params.email_tag_list, + link_tracking: params.link_tracking, + open_tracking: params.open_tracking, + stop_on_auto_reply: params.stop_on_auto_reply, + daily_max_leads: params.daily_max_leads, + prioritize_new_leads: params.prioritize_new_leads, + match_lead_esp: params.match_lead_esp, + stop_for_company: params.stop_for_company, + insert_unsubscribe_header: params.insert_unsubscribe_header, + allow_risky_contacts: params.allow_risky_contacts, + disable_bounce_protect: params.disable_bounce_protect, + cc_list: params.cc_list, + bcc_list: params.bcc_list, + owned_by: params.owned_by, + ai_sdr_id: params.ai_sdr_id, + }), + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + const campaign = mapCampaign(data) + + return { + success: true, + output: { + campaign, + id: campaign.id, + name: campaign.name, + status: campaign.status, + }, + } + }, + outputs: campaignOutputs, +} diff --git a/apps/sim/tools/instantly/create_lead.ts b/apps/sim/tools/instantly/create_lead.ts new file mode 100644 index 00000000000..a6c57db3956 --- /dev/null +++ b/apps/sim/tools/instantly/create_lead.ts @@ -0,0 +1,180 @@ +import type { InstantlyCreateLeadParams, InstantlyLeadResponse } from '@/tools/instantly/types' +import { + compactBody, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + leadOutputs, + mapLead, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const createLeadTool: ToolConfig = { + id: 'instantly_create_lead', + name: 'Instantly Create Lead', + description: 'Creates an Instantly V2 lead in a campaign or lead list.', + version: '1.0.0', + params: { + ...instantlyBaseParamFields, + campaign: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Campaign ID associated with the lead', + }, + list_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lead list ID associated with the lead', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lead email address. Required when adding to a campaign.', + }, + 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: 'Organization user ID assigned to the lead', + }, + skip_if_in_workspace: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Skip if the lead already exists in the workspace', + }, + skip_if_in_campaign: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Skip if the lead already exists in the campaign', + }, + skip_if_in_list: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Skip if the lead already exists in the list', + }, + blocklist_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Blocklist ID to check for the lead', + }, + verify_leads_on_import: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to verify leads on import', + }, + custom_variables: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Custom variable object with string, number, boolean, or null values', + }, + }, + request: { + url: () => instantlyUrl('/api/v2/leads'), + method: 'POST', + headers: instantlyHeaders, + body: (params) => + compactBody({ + campaign: params.campaign, + list_id: params.list_id, + email: params.email, + 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, + skip_if_in_workspace: params.skip_if_in_workspace, + skip_if_in_campaign: params.skip_if_in_campaign, + skip_if_in_list: params.skip_if_in_list, + blocklist_id: params.blocklist_id, + verify_leads_for_lead_finder: params.verify_leads_for_lead_finder, + verify_leads_on_import: params.verify_leads_on_import, + custom_variables: params.custom_variables, + }), + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + 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, +} diff --git a/apps/sim/tools/instantly/create_lead_list.ts b/apps/sim/tools/instantly/create_lead_list.ts new file mode 100644 index 00000000000..6a5c48ae20e --- /dev/null +++ b/apps/sim/tools/instantly/create_lead_list.ts @@ -0,0 +1,69 @@ +import type { + InstantlyCreateLeadListParams, + InstantlyLeadListResponse, +} from '@/tools/instantly/types' +import { + compactBody, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + leadListOutputs, + mapLeadList, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const createLeadListTool: ToolConfig< + InstantlyCreateLeadListParams, + InstantlyLeadListResponse +> = { + id: 'instantly_create_lead_list', + name: 'Instantly Create Lead List', + description: 'Creates an Instantly V2 lead list.', + version: '1.0.0', + params: { + ...instantlyBaseParamFields, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Lead list name', + }, + has_enrichment_task: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether this list runs enrichment for every added lead', + }, + owned_by: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User ID of the lead list owner', + }, + }, + request: { + url: () => instantlyUrl('/api/v2/lead-lists'), + method: 'POST', + headers: instantlyHeaders, + body: (params) => + compactBody({ + name: params.name, + has_enrichment_task: params.has_enrichment_task, + owned_by: params.owned_by, + }), + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + const leadList = mapLeadList(data) + + return { + success: true, + output: { + lead_list: leadList, + id: leadList.id, + name: leadList.name, + }, + } + }, + outputs: leadListOutputs, +} diff --git a/apps/sim/tools/instantly/delete_leads.ts b/apps/sim/tools/instantly/delete_leads.ts new file mode 100644 index 00000000000..8933510336f --- /dev/null +++ b/apps/sim/tools/instantly/delete_leads.ts @@ -0,0 +1,81 @@ +import type { + InstantlyDeleteLeadsParams, + InstantlyDeleteLeadsResponse, +} from '@/tools/instantly/types' +import { + asRecord, + compactBody, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteLeadsTool: ToolConfig = + { + id: 'instantly_delete_leads', + name: 'Instantly Delete Leads', + description: 'Deletes Instantly V2 leads in bulk from a campaign or lead list.', + version: '1.0.0', + params: { + ...instantlyBaseParamFields, + campaign_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Campaign ID to delete leads from. Required if list_id is not provided.', + }, + list_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lead list ID to delete leads from. Required if campaign_id is not provided.', + }, + status: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Optional lead status filter', + }, + ids: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Specific lead IDs to delete', + items: { type: 'string', description: 'Lead ID' }, + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of matching leads to delete, up to 10000', + }, + }, + request: { + url: () => instantlyUrl('/api/v2/leads'), + method: 'DELETE', + headers: instantlyHeaders, + body: (params) => + compactBody({ + campaign_id: params.campaign_id, + list_id: params.list_id, + status: params.status, + ids: params.ids, + limit: params.limit, + }), + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + const result = asRecord(data) + + return { + success: true, + output: { + count: typeof result.count === 'number' ? result.count : null, + }, + } + }, + outputs: { + count: { type: 'number', description: 'Number of leads deleted', optional: true }, + }, + } diff --git a/apps/sim/tools/instantly/get_lead.ts b/apps/sim/tools/instantly/get_lead.ts new file mode 100644 index 00000000000..e012c40d341 --- /dev/null +++ b/apps/sim/tools/instantly/get_lead.ts @@ -0,0 +1,48 @@ +import type { InstantlyGetLeadParams, InstantlyLeadResponse } from '@/tools/instantly/types' +import { + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + leadOutputs, + mapLead, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const getLeadTool: ToolConfig = { + id: 'instantly_get_lead', + name: 'Instantly Get Lead', + description: 'Retrieves an Instantly V2 lead by ID.', + version: '1.0.0', + params: { + ...instantlyBaseParamFields, + leadId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Lead ID', + }, + }, + request: { + url: (params) => instantlyUrl(`/api/v2/leads/${params.leadId}`), + method: 'GET', + headers: instantlyHeaders, + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + 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, +} diff --git a/apps/sim/tools/instantly/index.ts b/apps/sim/tools/instantly/index.ts new file mode 100644 index 00000000000..6af2ed62141 --- /dev/null +++ b/apps/sim/tools/instantly/index.ts @@ -0,0 +1,14 @@ +export { activateCampaignTool as instantlyActivateCampaignTool } from '@/tools/instantly/activate_campaign' +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 { 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' +export { listEmailsTool as instantlyListEmailsTool } from '@/tools/instantly/list_emails' +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 { 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' diff --git a/apps/sim/tools/instantly/list_campaigns.ts b/apps/sim/tools/instantly/list_campaigns.ts new file mode 100644 index 00000000000..c93b9885625 --- /dev/null +++ b/apps/sim/tools/instantly/list_campaigns.ts @@ -0,0 +1,90 @@ +import type { + InstantlyListCampaignsParams, + InstantlyListCampaignsResponse, +} from '@/tools/instantly/types' +import { + campaignsListOutputs, + getItems, + getNextStartingAfter, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + mapCampaign, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const listCampaignsTool: ToolConfig< + InstantlyListCampaignsParams, + InstantlyListCampaignsResponse +> = { + id: 'instantly_list_campaigns', + name: 'Instantly List Campaigns', + description: 'Retrieves Instantly V2 campaigns with search, status, tag, and pagination filters.', + version: '1.0.0', + params: { + ...instantlyBaseParamFields, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of campaigns to return, from 1 to 100', + }, + starting_after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from next_starting_after', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search by campaign name', + }, + tag_ids: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated campaign tag IDs', + }, + ai_sales_agent_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'AI Sales Agent ID filter', + }, + status: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Campaign status enum value', + }, + }, + request: { + url: (params) => + instantlyUrl('/api/v2/campaigns', { + limit: params.limit, + starting_after: params.starting_after, + search: params.search, + tag_ids: params.tag_ids, + ai_sales_agent_id: params.ai_sales_agent_id, + status: params.status, + }), + method: 'GET', + headers: instantlyHeaders, + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + const campaigns = getItems(data).map(mapCampaign) + + return { + success: true, + output: { + campaigns, + count: campaigns.length, + next_starting_after: getNextStartingAfter(data), + }, + } + }, + outputs: campaignsListOutputs, +} diff --git a/apps/sim/tools/instantly/list_emails.ts b/apps/sim/tools/instantly/list_emails.ts new file mode 100644 index 00000000000..94e2b47e5ab --- /dev/null +++ b/apps/sim/tools/instantly/list_emails.ts @@ -0,0 +1,115 @@ +import type { + InstantlyListEmailsParams, + InstantlyListEmailsResponse, +} from '@/tools/instantly/types' +import { + emailsListOutputs, + getItems, + getNextStartingAfter, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + mapEmail, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const listEmailsTool: ToolConfig = { + id: 'instantly_list_emails', + name: 'Instantly List Emails', + description: 'Retrieves Instantly V2 Unibox emails with search and pagination filters.', + version: '1.0.0', + params: { + ...instantlyBaseParamFields, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of emails to return, from 1 to 100', + }, + starting_after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from next_starting_after', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query, email address, or thread:', + }, + campaign_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Campaign ID filter', + }, + list_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lead list ID filter', + }, + i_status: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Email interest status filter', + }, + eaccount: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sending email account filter', + }, + lead: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lead email address filter', + }, + lead_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lead ID filter', + }, + is_unread: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Unread status filter', + }, + }, + request: { + url: (params) => + instantlyUrl('/api/v2/emails', { + limit: params.limit, + starting_after: params.starting_after, + search: params.thread_id ? `thread:${params.thread_id}` : params.search, + campaign_id: params.campaign_id, + list_id: params.list_id, + i_status: params.i_status, + eaccount: params.eaccount, + lead: params.lead, + lead_id: params.lead_id, + is_unread: params.is_unread, + }), + method: 'GET', + headers: instantlyHeaders, + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + const emails = getItems(data).map(mapEmail) + + return { + success: true, + output: { + emails, + count: emails.length, + next_starting_after: getNextStartingAfter(data), + }, + } + }, + outputs: emailsListOutputs, +} diff --git a/apps/sim/tools/instantly/list_lead_lists.ts b/apps/sim/tools/instantly/list_lead_lists.ts new file mode 100644 index 00000000000..0456fc3ff7d --- /dev/null +++ b/apps/sim/tools/instantly/list_lead_lists.ts @@ -0,0 +1,76 @@ +import type { + InstantlyListLeadListsParams, + InstantlyListLeadListsResponse, +} from '@/tools/instantly/types' +import { + getItems, + getNextStartingAfter, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + leadListsListOutputs, + mapLeadList, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const listLeadListsTool: ToolConfig< + InstantlyListLeadListsParams, + InstantlyListLeadListsResponse +> = { + id: 'instantly_list_lead_lists', + name: 'Instantly List Lead Lists', + description: 'Retrieves Instantly V2 lead lists with search and pagination filters.', + version: '1.0.0', + params: { + ...instantlyBaseParamFields, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of lead lists to return, from 1 to 100', + }, + starting_after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Starting-after timestamp cursor', + }, + has_enrichment_task: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Filter by enrichment task setting', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query to filter lead lists by name', + }, + }, + request: { + url: (params) => + instantlyUrl('/api/v2/lead-lists', { + limit: params.limit, + starting_after: params.starting_after, + has_enrichment_task: params.has_enrichment_task, + search: params.search, + }), + method: 'GET', + headers: instantlyHeaders, + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + const leadLists = getItems(data).map(mapLeadList) + + return { + success: true, + output: { + lead_lists: leadLists, + count: leadLists.length, + next_starting_after: getNextStartingAfter(data), + }, + } + }, + outputs: leadListsListOutputs, +} diff --git a/apps/sim/tools/instantly/list_leads.ts b/apps/sim/tools/instantly/list_leads.ts new file mode 100644 index 00000000000..1f81cd1ba85 --- /dev/null +++ b/apps/sim/tools/instantly/list_leads.ts @@ -0,0 +1,148 @@ +import type { InstantlyListLeadsParams, InstantlyListLeadsResponse } from '@/tools/instantly/types' +import { + compactBody, + getItems, + getNextStartingAfter, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + leadsListOutputs, + mapLead, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const listLeadsTool: ToolConfig = { + id: 'instantly_list_leads', + name: 'Instantly List Leads', + description: 'Retrieves Instantly V2 leads with search, campaign, list, and pagination filters.', + version: '1.0.0', + params: { + ...instantlyBaseParamFields, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search by first name, last name, or email', + }, + filter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Instantly lead filter value, such as FILTER_VAL_CONTACTED or FILTER_VAL_ACTIVE', + }, + campaign: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Campaign ID to filter leads', + }, + list_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lead list ID to filter leads', + }, + in_campaign: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the lead is in a campaign', + }, + in_list: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the lead is in a list', + }, + ids: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Lead IDs to include', + items: { type: 'string', description: 'Lead ID' }, + }, + excluded_ids: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Lead IDs to exclude', + items: { type: 'string', description: 'Lead ID' }, + }, + contacts: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Lead email addresses to include', + items: { type: 'string', description: 'Email address' }, + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of leads to return, from 1 to 100', + }, + starting_after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Forward pagination cursor from next_starting_after', + }, + distinct_contacts: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to return distinct contacts', + }, + enrichment_status: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Enrichment status filter', + }, + esg_code: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email security gateway code filter', + }, + }, + request: { + url: () => instantlyUrl('/api/v2/leads/list'), + method: 'POST', + headers: instantlyHeaders, + body: (params) => + compactBody({ + search: params.search, + filter: params.filter, + campaign: params.campaign, + list_id: params.list_id, + in_campaign: params.in_campaign, + in_list: params.in_list, + ids: params.ids, + excluded_ids: params.excluded_ids, + contacts: params.contacts, + limit: params.limit, + starting_after: params.starting_after, + organization_user_ids: params.organization_user_ids, + smart_view_id: params.smart_view_id, + is_website_visitor: params.is_website_visitor, + distinct_contacts: params.distinct_contacts, + enrichment_status: params.enrichment_status, + esg_code: params.esg_code, + }), + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + const leads = getItems(data).map(mapLead) + + return { + success: true, + output: { + leads, + count: leads.length, + next_starting_after: getNextStartingAfter(data), + }, + } + }, + outputs: leadsListOutputs, +} diff --git a/apps/sim/tools/instantly/patch_campaign.ts b/apps/sim/tools/instantly/patch_campaign.ts new file mode 100644 index 00000000000..26609b50597 --- /dev/null +++ b/apps/sim/tools/instantly/patch_campaign.ts @@ -0,0 +1,156 @@ +import type { + InstantlyCampaignResponse, + InstantlyPatchCampaignParams, +} from '@/tools/instantly/types' +import { + campaignOutputs, + compactBody, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + mapCampaign, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const patchCampaignTool: ToolConfig< + InstantlyPatchCampaignParams, + InstantlyCampaignResponse +> = { + id: 'instantly_patch_campaign', + name: 'Instantly Patch Campaign', + description: 'Updates documented Instantly V2 campaign fields.', + version: '1.0.0', + params: { + ...instantlyBaseParamFields, + campaignId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Campaign ID', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Campaign name', + }, + campaign_schedule: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Campaign schedule object with schedules array', + }, + sequences: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Campaign sequence definitions', + items: { type: 'object', description: 'Sequence object' }, + }, + email_list: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Sending email accounts', + items: { type: 'string', description: 'Email address' }, + }, + daily_limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Daily sending limit', + }, + daily_max_leads: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Daily maximum new leads to contact', + }, + open_tracking: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to track opens', + }, + stop_on_reply: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to stop the campaign on reply', + }, + link_tracking: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to track links', + }, + text_only: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the campaign is text only', + }, + email_gap: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Gap between emails in minutes', + }, + pl_value: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Value of every positive lead', + }, + }, + request: { + url: (params) => instantlyUrl(`/api/v2/campaigns/${params.campaignId}`), + method: 'PATCH', + headers: instantlyHeaders, + body: (params) => + compactBody({ + name: params.name, + campaign_schedule: params.campaign_schedule, + sequences: params.sequences, + pl_value: params.pl_value, + is_evergreen: params.is_evergreen, + email_gap: params.email_gap, + random_wait_max: params.random_wait_max, + text_only: params.text_only, + first_email_text_only: params.first_email_text_only, + email_list: params.email_list, + daily_limit: params.daily_limit, + stop_on_reply: params.stop_on_reply, + email_tag_list: params.email_tag_list, + link_tracking: params.link_tracking, + open_tracking: params.open_tracking, + stop_on_auto_reply: params.stop_on_auto_reply, + daily_max_leads: params.daily_max_leads, + prioritize_new_leads: params.prioritize_new_leads, + match_lead_esp: params.match_lead_esp, + stop_for_company: params.stop_for_company, + insert_unsubscribe_header: params.insert_unsubscribe_header, + allow_risky_contacts: params.allow_risky_contacts, + disable_bounce_protect: params.disable_bounce_protect, + cc_list: params.cc_list, + bcc_list: params.bcc_list, + owned_by: params.owned_by, + ai_sdr_id: params.ai_sdr_id, + }), + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + const campaign = mapCampaign(data) + + return { + success: true, + output: { + campaign, + id: campaign.id, + name: campaign.name, + status: campaign.status, + }, + } + }, + outputs: campaignOutputs, +} diff --git a/apps/sim/tools/instantly/reply_to_email.ts b/apps/sim/tools/instantly/reply_to_email.ts new file mode 100644 index 00000000000..fe201a18914 --- /dev/null +++ b/apps/sim/tools/instantly/reply_to_email.ts @@ -0,0 +1,85 @@ +import type { InstantlyEmailResponse, InstantlyReplyToEmailParams } from '@/tools/instantly/types' +import { + compactBody, + emailOutputs, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + mapEmail, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const replyToEmailTool: ToolConfig = { + id: 'instantly_reply_to_email', + name: 'Instantly Reply To Email', + description: 'Sends an Instantly V2 reply to an existing Unibox email.', + version: '1.0.0', + params: { + ...instantlyBaseParamFields, + eaccount: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Connected email account used to send the reply', + }, + reply_to_uuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email ID to reply to', + }, + subject: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Reply subject', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Reply body object with text and/or html', + }, + cc_address_email_list: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated CC email addresses', + }, + bcc_address_email_list: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated BCC email addresses', + }, + }, + request: { + url: () => instantlyUrl('/api/v2/emails/reply'), + method: 'POST', + headers: instantlyHeaders, + body: (params) => + compactBody({ + eaccount: params.eaccount, + reply_to_uuid: params.reply_to_uuid, + subject: params.subject, + body: params.body, + cc_address_email_list: params.cc_address_email_list, + bcc_address_email_list: params.bcc_address_email_list, + }), + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + const email = mapEmail(data) + + return { + success: true, + output: { + email, + id: email.id, + subject: email.subject, + thread_id: email.thread_id, + }, + } + }, + outputs: emailOutputs, +} diff --git a/apps/sim/tools/instantly/types.ts b/apps/sim/tools/instantly/types.ts new file mode 100644 index 00000000000..fe5575289c6 --- /dev/null +++ b/apps/sim/tools/instantly/types.ts @@ -0,0 +1,331 @@ +import type { ToolResponse } from '@/tools/types' + +export type InstantlyScalar = string | number | boolean | null + +export interface InstantlyBaseParams { + apiKey: string +} + +export interface InstantlyLead { + id: string | null + timestamp_created: string | null + timestamp_updated: string | null + organization: string | null + campaign: string | null + status: number | null + email: string | null + 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 + email_open_count: number | null + email_reply_count: number | null + email_click_count: number | null + company_domain: string | null + payload: Record | null + lt_interest_status: number | null +} + +export interface InstantlyCampaign { + id: string | null + name: string | null + pl_value: number | null + status: number | null + is_evergreen: boolean | null + timestamp_created: string | null + timestamp_updated: string | null + email_gap: number | null + daily_limit: number | null + daily_max_leads: number | null + open_tracking: boolean | null + stop_on_reply: boolean | null + sequences: unknown[] + campaign_schedule: Record | null +} + +export interface InstantlyEmail { + id: string | null + timestamp_created: string | null + timestamp_email: string | null + message_id: string | null + subject: string | null + from_address_email: string | null + to_address_email_list: string | null + cc_address_email_list: string | null + bcc_address_email_list: string | null + reply_to: string | null + body: { + text: string | null + html: string | null + } + organization_id: string | null + campaign_id: string | null + subsequence_id: string | null + list_id: string | null + lead: string | null + lead_id: string | null + eaccount: string | null + ue_type: number | null + is_unread: number | null + is_auto_reply: number | null + i_status: number | null + thread_id: string | null + content_preview: string | null +} + +export interface InstantlyLeadList { + id: string | null + organization_id: string | null + has_enrichment_task: boolean | null + owned_by: string | null + name: string | null + timestamp_created: string | null +} + +export interface InstantlyListLeadsParams extends InstantlyBaseParams { + search?: string + filter?: string + campaign?: string + list_id?: string + in_campaign?: boolean + in_list?: boolean + ids?: string[] + excluded_ids?: string[] + contacts?: string[] + limit?: number + starting_after?: string + organization_user_ids?: string[] + smart_view_id?: string + is_website_visitor?: boolean + distinct_contacts?: boolean + enrichment_status?: number + esg_code?: string +} + +export interface InstantlyGetLeadParams extends InstantlyBaseParams { + leadId: string +} + +export interface InstantlyCreateLeadParams extends InstantlyBaseParams { + campaign?: string | null + email?: string | null + 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 + list_id?: string | null + assigned_to?: string | null + skip_if_in_workspace?: boolean + skip_if_in_campaign?: boolean + skip_if_in_list?: boolean + blocklist_id?: string + verify_leads_for_lead_finder?: boolean + verify_leads_on_import?: boolean + custom_variables?: Record +} + +export interface InstantlyDeleteLeadsParams extends InstantlyBaseParams { + campaign_id?: string + list_id?: string + status?: number + ids?: string[] + limit?: number +} + +export interface InstantlyUpdateLeadInterestStatusParams extends InstantlyBaseParams { + lead_email: string + interest_value: number | null + campaign_id?: string + ai_interest_value?: number + disable_auto_interest?: boolean + list_id?: string +} + +export interface InstantlyListCampaignsParams extends InstantlyBaseParams { + limit?: number + starting_after?: string + search?: string + tag_ids?: string + ai_sales_agent_id?: string + status?: number +} + +export interface InstantlyCreateCampaignParams extends InstantlyBaseParams { + name: string + campaign_schedule: Record + sequences?: unknown[] + pl_value?: number | null + is_evergreen?: boolean | null + email_gap?: number | null + random_wait_max?: number | null + text_only?: boolean | null + first_email_text_only?: boolean | null + email_list?: string[] + daily_limit?: number | null + stop_on_reply?: boolean | null + email_tag_list?: string[] + link_tracking?: boolean | null + open_tracking?: boolean + stop_on_auto_reply?: boolean | null + daily_max_leads?: number | null + prioritize_new_leads?: boolean | null + match_lead_esp?: boolean | null + stop_for_company?: boolean | null + insert_unsubscribe_header?: boolean | null + allow_risky_contacts?: boolean | null + disable_bounce_protect?: boolean | null + cc_list?: string[] + bcc_list?: string[] + owned_by?: string | null + ai_sdr_id?: string | null +} + +export interface InstantlyPatchCampaignParams extends Partial { + apiKey: string + campaignId: string +} + +export interface InstantlyActivateCampaignParams extends InstantlyBaseParams { + campaignId: string +} + +export interface InstantlyListEmailsParams extends InstantlyBaseParams { + limit?: number + starting_after?: string + search?: string + campaign_id?: string + list_id?: string + i_status?: number + eaccount?: string + lead?: string + lead_id?: string + is_unread?: number + thread_id?: string +} + +export interface InstantlyReplyToEmailParams extends InstantlyBaseParams { + eaccount: string + reply_to_uuid: string + subject: string + body: { + text?: string + html?: string + } + cc_address_email_list?: string + bcc_address_email_list?: string +} + +export interface InstantlyListLeadListsParams extends InstantlyBaseParams { + limit?: number + starting_after?: string + has_enrichment_task?: boolean + search?: string +} + +export interface InstantlyCreateLeadListParams extends InstantlyBaseParams { + name: string + has_enrichment_task?: boolean | null + owned_by?: string | null +} + +export interface InstantlyListLeadsResponse extends ToolResponse { + output: { + leads: InstantlyLead[] + count: number + next_starting_after: string | null + } +} + +export interface InstantlyLeadResponse extends ToolResponse { + output: { + lead: InstantlyLead + id: string | null + email_address: string | null + first_name: string | null + last_name: string | null + campaign: string | null + status: number | null + } +} + +export interface InstantlyDeleteLeadsResponse extends ToolResponse { + output: { + count: number | null + } +} + +export interface InstantlyUpdateLeadInterestStatusResponse extends ToolResponse { + output: { + message: string | null + } +} + +export interface InstantlyListCampaignsResponse extends ToolResponse { + output: { + campaigns: InstantlyCampaign[] + count: number + next_starting_after: string | null + } +} + +export interface InstantlyCampaignResponse extends ToolResponse { + output: { + campaign: InstantlyCampaign + id: string | null + name: string | null + status: number | null + } +} + +export interface InstantlyListEmailsResponse extends ToolResponse { + output: { + emails: InstantlyEmail[] + count: number + next_starting_after: string | null + } +} + +export interface InstantlyEmailResponse extends ToolResponse { + output: { + email: InstantlyEmail + id: string | null + subject: string | null + thread_id: string | null + } +} + +export interface InstantlyListLeadListsResponse extends ToolResponse { + output: { + lead_lists: InstantlyLeadList[] + count: number + next_starting_after: string | null + } +} + +export interface InstantlyLeadListResponse extends ToolResponse { + output: { + lead_list: InstantlyLeadList + id: string | null + name: string | null + } +} + +export type InstantlyResponse = + | InstantlyListLeadsResponse + | InstantlyLeadResponse + | InstantlyDeleteLeadsResponse + | InstantlyUpdateLeadInterestStatusResponse + | InstantlyListCampaignsResponse + | InstantlyCampaignResponse + | InstantlyListEmailsResponse + | InstantlyEmailResponse + | InstantlyListLeadListsResponse + | InstantlyLeadListResponse diff --git a/apps/sim/tools/instantly/update_lead_interest_status.ts b/apps/sim/tools/instantly/update_lead_interest_status.ts new file mode 100644 index 00000000000..1b7d9907c74 --- /dev/null +++ b/apps/sim/tools/instantly/update_lead_interest_status.ts @@ -0,0 +1,93 @@ +import type { + InstantlyUpdateLeadInterestStatusParams, + InstantlyUpdateLeadInterestStatusResponse, +} from '@/tools/instantly/types' +import { + asRecord, + compactBody, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, +} from '@/tools/instantly/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateLeadInterestStatusTool: ToolConfig< + InstantlyUpdateLeadInterestStatusParams, + InstantlyUpdateLeadInterestStatusResponse +> = { + id: 'instantly_update_lead_interest_status', + name: 'Instantly Update Lead Interest Status', + description: 'Submits an Instantly V2 background job to update a lead interest status.', + version: '1.0.0', + params: { + ...instantlyBaseParamFields, + lead_email: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Lead email address', + }, + interest_value: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Interest status value. Use null in JSON/tool input to reset to Lead.', + }, + campaign_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Campaign ID for the lead', + }, + list_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lead list ID for the lead', + }, + ai_interest_value: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'AI interest value to set for the lead', + }, + disable_auto_interest: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to disable auto interest', + }, + }, + request: { + url: () => instantlyUrl('/api/v2/leads/update-interest-status'), + method: 'POST', + headers: instantlyHeaders, + body: (params) => + compactBody({ + lead_email: params.lead_email, + interest_value: params.interest_value, + campaign_id: params.campaign_id, + list_id: params.list_id, + ai_interest_value: params.ai_interest_value, + disable_auto_interest: params.disable_auto_interest, + }), + }, + transformResponse: async (response) => { + const data: unknown = await response.json() + const result = asRecord(data) + + return { + success: true, + output: { + message: typeof result.message === 'string' ? result.message : null, + }, + } + }, + outputs: { + message: { + type: 'string', + description: 'Background job submission message', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/instantly/utils.ts b/apps/sim/tools/instantly/utils.ts new file mode 100644 index 00000000000..192f4c8fde8 --- /dev/null +++ b/apps/sim/tools/instantly/utils.ts @@ -0,0 +1,339 @@ +import { filterUndefined } from '@sim/utils/object' +import type { + InstantlyCampaign, + InstantlyEmail, + InstantlyLead, + InstantlyLeadList, +} from '@/tools/instantly/types' +import type { ToolConfig } from '@/tools/types' + +const INSTANTLY_API_BASE_URL = 'https://api.instantly.ai' + +type InstantlyBaseParams = { apiKey: string } +type JsonRecord = Record + +export const instantlyBaseParamFields = { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Instantly API key with the required V2 scopes', + }, +} satisfies ToolConfig['params'] + +export const instantlyHeaders = (params: InstantlyBaseParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', +}) + +export function instantlyUrl(path: string, query?: Record): string { + const url = new URL(path, INSTANTLY_API_BASE_URL) + + if (query) { + for (const [key, value] of Object.entries(query)) { + if (value === undefined || value === null || value === '') continue + url.searchParams.append(key, String(value)) + } + } + + return url.toString() +} + +export function compactBody(values: Record): Record { + return filterUndefined(values) +} + +export function asRecord(value: unknown): JsonRecord { + return isRecord(value) ? value : {} +} + +export function getItems(value: unknown): JsonRecord[] { + const data = asRecord(value) + return Array.isArray(data.items) ? data.items.map(asRecord) : [] +} + +export function getNextStartingAfter(value: unknown): string | null { + const data = asRecord(value) + return asString(data.next_starting_after) +} + +export function mapLead(value: unknown): InstantlyLead { + const lead = asRecord(value) + + return { + id: asString(lead.id), + timestamp_created: asString(lead.timestamp_created), + timestamp_updated: asString(lead.timestamp_updated), + organization: asString(lead.organization), + campaign: asString(lead.campaign), + status: asNumber(lead.status), + email: asString(lead.email), + personalization: asString(lead.personalization), + website: asString(lead.website), + last_name: asString(lead.last_name), + first_name: asString(lead.first_name), + company_name: asString(lead.company_name), + job_title: asString(lead.job_title), + phone: asString(lead.phone), + email_open_count: asNumber(lead.email_open_count), + email_reply_count: asNumber(lead.email_reply_count), + email_click_count: asNumber(lead.email_click_count), + company_domain: asString(lead.company_domain), + payload: isRecord(lead.payload) ? lead.payload : null, + lt_interest_status: asNumber(lead.lt_interest_status), + } +} + +export function mapCampaign(value: unknown): InstantlyCampaign { + const campaign = asRecord(value) + + return { + id: asString(campaign.id), + name: asString(campaign.name), + pl_value: asNumber(campaign.pl_value), + status: asNumber(campaign.status), + is_evergreen: asBoolean(campaign.is_evergreen), + timestamp_created: asString(campaign.timestamp_created), + timestamp_updated: asString(campaign.timestamp_updated), + email_gap: asNumber(campaign.email_gap), + daily_limit: asNumber(campaign.daily_limit), + daily_max_leads: asNumber(campaign.daily_max_leads), + open_tracking: asBoolean(campaign.open_tracking), + stop_on_reply: asBoolean(campaign.stop_on_reply), + sequences: Array.isArray(campaign.sequences) ? campaign.sequences : [], + campaign_schedule: isRecord(campaign.campaign_schedule) ? campaign.campaign_schedule : null, + } +} + +export function mapEmail(value: unknown): InstantlyEmail { + const email = asRecord(value) + const body = asRecord(email.body) + + return { + id: asString(email.id), + timestamp_created: asString(email.timestamp_created), + timestamp_email: asString(email.timestamp_email), + message_id: asString(email.message_id), + subject: asString(email.subject), + from_address_email: asString(email.from_address_email), + to_address_email_list: asString(email.to_address_email_list), + cc_address_email_list: asString(email.cc_address_email_list), + bcc_address_email_list: asString(email.bcc_address_email_list), + reply_to: asString(email.reply_to), + body: { + text: asString(body.text), + html: asString(body.html), + }, + organization_id: asString(email.organization_id), + campaign_id: asString(email.campaign_id), + subsequence_id: asString(email.subsequence_id), + list_id: asString(email.list_id), + lead: asString(email.lead), + lead_id: asString(email.lead_id), + eaccount: asString(email.eaccount), + ue_type: asNumber(email.ue_type), + is_unread: asNumber(email.is_unread), + is_auto_reply: asNumber(email.is_auto_reply), + i_status: asNumber(email.i_status), + thread_id: asString(email.thread_id), + content_preview: asString(email.content_preview), + } +} + +export function mapLeadList(value: unknown): InstantlyLeadList { + const leadList = asRecord(value) + + return { + id: asString(leadList.id), + organization_id: asString(leadList.organization_id), + has_enrichment_task: asBoolean(leadList.has_enrichment_task), + owned_by: asString(leadList.owned_by), + name: asString(leadList.name), + timestamp_created: asString(leadList.timestamp_created), + } +} + +export const leadOutputs = { + lead: { + type: 'object', + description: 'Lead details', + properties: { + id: { type: 'string', description: 'Lead ID', nullable: true }, + email: { type: 'string', description: 'Lead email address', nullable: true }, + first_name: { type: 'string', description: 'Lead first name', nullable: true }, + last_name: { type: 'string', description: 'Lead last name', nullable: true }, + company_name: { type: 'string', description: 'Lead company name', nullable: true }, + job_title: { type: 'string', description: 'Lead job title', nullable: true }, + campaign: { type: 'string', description: 'Campaign ID', nullable: true }, + status: { type: 'number', description: 'Lead status', nullable: true }, + payload: { type: 'json', description: 'Lead custom variables', nullable: true }, + }, + }, + id: { type: 'string', description: 'Lead ID', optional: true }, + email_address: { type: 'string', description: 'Lead email address', optional: true }, + first_name: { type: 'string', description: 'Lead first name', optional: true }, + last_name: { type: 'string', description: 'Lead last name', optional: true }, + campaign: { type: 'string', description: 'Campaign ID', optional: true }, + status: { type: 'number', description: 'Lead status', optional: true }, +} satisfies ToolConfig['outputs'] + +export const leadsListOutputs = { + leads: { + type: 'array', + description: 'List of leads', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Lead ID', nullable: true }, + email: { type: 'string', description: 'Lead email address', nullable: true }, + first_name: { type: 'string', description: 'Lead first name', nullable: true }, + last_name: { type: 'string', description: 'Lead last name', nullable: true }, + company_name: { type: 'string', description: 'Lead company name', nullable: true }, + campaign: { type: 'string', description: 'Campaign ID', nullable: true }, + status: { type: 'number', description: 'Lead status', nullable: true }, + }, + }, + }, + count: { type: 'number', description: 'Number of leads returned' }, + next_starting_after: { type: 'string', description: 'Cursor for the next page', optional: true }, +} satisfies ToolConfig['outputs'] + +export const campaignOutputs = { + campaign: { + type: 'object', + description: 'Campaign details', + properties: { + id: { type: 'string', description: 'Campaign ID', nullable: true }, + name: { type: 'string', description: 'Campaign name', nullable: true }, + status: { type: 'number', description: 'Campaign status', nullable: true }, + daily_limit: { type: 'number', description: 'Daily sending limit', nullable: true }, + daily_max_leads: { type: 'number', description: 'Daily max new leads', nullable: true }, + open_tracking: { + type: 'boolean', + description: 'Whether open tracking is enabled', + nullable: true, + }, + }, + }, + id: { type: 'string', description: 'Campaign ID', optional: true }, + name: { type: 'string', description: 'Campaign name', optional: true }, + status: { type: 'number', description: 'Campaign status', optional: true }, +} satisfies ToolConfig['outputs'] + +export const campaignsListOutputs = { + campaigns: { + type: 'array', + description: 'List of campaigns', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Campaign ID', nullable: true }, + name: { type: 'string', description: 'Campaign name', nullable: true }, + status: { type: 'number', description: 'Campaign status', nullable: true }, + daily_limit: { type: 'number', description: 'Daily sending limit', nullable: true }, + }, + }, + }, + count: { type: 'number', description: 'Number of campaigns returned' }, + next_starting_after: { type: 'string', description: 'Cursor for the next page', optional: true }, +} satisfies ToolConfig['outputs'] + +export const emailOutputs = { + email: { + type: 'object', + description: 'Email details', + properties: { + id: { type: 'string', description: 'Email ID', nullable: true }, + subject: { type: 'string', description: 'Email subject', nullable: true }, + from_address_email: { type: 'string', description: 'Sender email', nullable: true }, + to_address_email_list: { + type: 'string', + description: 'Recipient email list', + nullable: true, + }, + thread_id: { type: 'string', description: 'Thread ID', nullable: true }, + content_preview: { type: 'string', description: 'Email content preview', nullable: true }, + }, + }, + id: { type: 'string', description: 'Email ID', optional: true }, + subject: { type: 'string', description: 'Email subject', optional: true }, + thread_id: { type: 'string', description: 'Thread ID', optional: true }, +} satisfies ToolConfig['outputs'] + +export const emailsListOutputs = { + emails: { + type: 'array', + description: 'List of emails', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Email ID', nullable: true }, + subject: { type: 'string', description: 'Email subject', nullable: true }, + from_address_email: { type: 'string', description: 'Sender email', nullable: true }, + lead: { type: 'string', description: 'Lead email', nullable: true }, + thread_id: { type: 'string', description: 'Thread ID', nullable: true }, + }, + }, + }, + count: { type: 'number', description: 'Number of emails returned' }, + next_starting_after: { type: 'string', description: 'Cursor for the next page', optional: true }, +} satisfies ToolConfig['outputs'] + +export const leadListOutputs = { + lead_list: { + type: 'object', + description: 'Lead list details', + properties: { + id: { type: 'string', description: 'Lead list ID', nullable: true }, + organization_id: { type: 'string', description: 'Organization ID', nullable: true }, + has_enrichment_task: { + type: 'boolean', + description: 'Whether enrichment is enabled', + nullable: true, + }, + owned_by: { type: 'string', description: 'Owner user ID', nullable: true }, + name: { type: 'string', description: 'Lead list name', nullable: true }, + timestamp_created: { type: 'string', description: 'Creation timestamp', nullable: true }, + }, + }, + id: { type: 'string', description: 'Lead list ID', optional: true }, + name: { type: 'string', description: 'Lead list name', optional: true }, +} satisfies ToolConfig['outputs'] + +export const leadListsListOutputs = { + lead_lists: { + type: 'array', + description: 'List of lead lists', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Lead list ID', nullable: true }, + name: { type: 'string', description: 'Lead list name', nullable: true }, + has_enrichment_task: { + type: 'boolean', + description: 'Whether enrichment is enabled', + nullable: true, + }, + timestamp_created: { type: 'string', description: 'Creation timestamp', nullable: true }, + }, + }, + }, + count: { type: 'number', description: 'Number of lead lists returned' }, + next_starting_after: { type: 'string', description: 'Cursor for the next page', optional: true }, +} satisfies ToolConfig['outputs'] + +function isRecord(value: unknown): value is JsonRecord { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function asString(value: unknown): string | null { + return typeof value === 'string' ? value : null +} + +function asNumber(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? value : null +} + +function asBoolean(value: unknown): boolean | null { + return typeof value === 'boolean' ? value : null +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 54e16312802..3133260d9cc 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1390,6 +1390,21 @@ import { infisicalListSecretsTool, infisicalUpdateSecretTool, } from '@/tools/infisical' +import { + instantlyActivateCampaignTool, + instantlyCreateCampaignTool, + instantlyCreateLeadListTool, + instantlyCreateLeadTool, + instantlyDeleteLeadsTool, + instantlyGetLeadTool, + instantlyListCampaignsTool, + instantlyListEmailsTool, + instantlyListLeadListsTool, + instantlyListLeadsTool, + instantlyPatchCampaignTool, + instantlyReplyToEmailTool, + instantlyUpdateLeadInterestStatusTool, +} from '@/tools/instantly' import { intercomAssignConversationV2Tool, intercomAttachContactToCompanyV2Tool, @@ -3434,6 +3449,19 @@ export const tools: Record = { hex_list_users: hexListUsersTool, hex_run_project: hexRunProjectTool, hex_update_project: hexUpdateProjectTool, + instantly_activate_campaign: instantlyActivateCampaignTool, + instantly_create_campaign: instantlyCreateCampaignTool, + instantly_create_lead: instantlyCreateLeadTool, + instantly_create_lead_list: instantlyCreateLeadListTool, + instantly_delete_leads: instantlyDeleteLeadsTool, + instantly_get_lead: instantlyGetLeadTool, + instantly_list_campaigns: instantlyListCampaignsTool, + instantly_list_emails: instantlyListEmailsTool, + instantly_list_lead_lists: instantlyListLeadListsTool, + instantly_list_leads: instantlyListLeadsTool, + instantly_patch_campaign: instantlyPatchCampaignTool, + instantly_reply_to_email: instantlyReplyToEmailTool, + instantly_update_lead_interest_status: instantlyUpdateLeadInterestStatusTool, jina_read_url: jinaReadUrlTool, jina_search: jinaSearchTool, ketch_get_consent: ketchGetConsentTool, diff --git a/apps/sim/triggers/instantly/account_error.ts b/apps/sim/triggers/instantly/account_error.ts new file mode 100644 index 00000000000..896c3fe34bb --- /dev/null +++ b/apps/sim/triggers/instantly/account_error.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyAccountErrorTrigger = createInstantlyTrigger({ + id: 'instantly_account_error', + name: 'Instantly Account Error', + description: 'Trigger when Instantly reports an account-level error', + eventLabel: 'Account Error', +}) diff --git a/apps/sim/triggers/instantly/auto_reply_received.ts b/apps/sim/triggers/instantly/auto_reply_received.ts new file mode 100644 index 00000000000..647d1f8b4ec --- /dev/null +++ b/apps/sim/triggers/instantly/auto_reply_received.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyAutoReplyReceivedTrigger = createInstantlyTrigger({ + id: 'instantly_auto_reply_received', + name: 'Instantly Auto Reply Received', + description: 'Trigger when Instantly receives an auto-reply from a lead', + eventLabel: 'Auto Reply Received', +}) diff --git a/apps/sim/triggers/instantly/campaign_completed.ts b/apps/sim/triggers/instantly/campaign_completed.ts new file mode 100644 index 00000000000..ffc06a48207 --- /dev/null +++ b/apps/sim/triggers/instantly/campaign_completed.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyCampaignCompletedTrigger = createInstantlyTrigger({ + id: 'instantly_campaign_completed', + name: 'Instantly Campaign Completed', + description: 'Trigger when an Instantly campaign completes', + eventLabel: 'Campaign Completed', +}) diff --git a/apps/sim/triggers/instantly/email_bounced.ts b/apps/sim/triggers/instantly/email_bounced.ts new file mode 100644 index 00000000000..291bbe8365c --- /dev/null +++ b/apps/sim/triggers/instantly/email_bounced.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyEmailBouncedTrigger = createInstantlyTrigger({ + id: 'instantly_email_bounced', + name: 'Instantly Email Bounced', + description: 'Trigger when an Instantly email bounces', + eventLabel: 'Email Bounced', +}) diff --git a/apps/sim/triggers/instantly/email_opened.ts b/apps/sim/triggers/instantly/email_opened.ts new file mode 100644 index 00000000000..66c6f2e2076 --- /dev/null +++ b/apps/sim/triggers/instantly/email_opened.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyEmailOpenedTrigger = createInstantlyTrigger({ + id: 'instantly_email_opened', + name: 'Instantly Email Opened', + description: 'Trigger when a lead opens an Instantly email', + eventLabel: 'Email Opened', +}) diff --git a/apps/sim/triggers/instantly/email_sent.ts b/apps/sim/triggers/instantly/email_sent.ts new file mode 100644 index 00000000000..57ae784d0e6 --- /dev/null +++ b/apps/sim/triggers/instantly/email_sent.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyEmailSentTrigger = createInstantlyTrigger({ + id: 'instantly_email_sent', + name: 'Instantly Email Sent', + description: 'Trigger when Instantly sends an email', + eventLabel: 'Email Sent', +}) diff --git a/apps/sim/triggers/instantly/index.ts b/apps/sim/triggers/instantly/index.ts new file mode 100644 index 00000000000..540e74e9764 --- /dev/null +++ b/apps/sim/triggers/instantly/index.ts @@ -0,0 +1,20 @@ +export { instantlyAccountErrorTrigger } from '@/triggers/instantly/account_error' +export { instantlyAutoReplyReceivedTrigger } from '@/triggers/instantly/auto_reply_received' +export { instantlyCampaignCompletedTrigger } from '@/triggers/instantly/campaign_completed' +export { instantlyEmailBouncedTrigger } from '@/triggers/instantly/email_bounced' +export { instantlyEmailOpenedTrigger } from '@/triggers/instantly/email_opened' +export { instantlyEmailSentTrigger } from '@/triggers/instantly/email_sent' +export { instantlyLeadClosedTrigger } from '@/triggers/instantly/lead_closed' +export { instantlyLeadInterestedTrigger } from '@/triggers/instantly/lead_interested' +export { instantlyLeadMeetingBookedTrigger } from '@/triggers/instantly/lead_meeting_booked' +export { instantlyLeadMeetingCompletedTrigger } from '@/triggers/instantly/lead_meeting_completed' +export { instantlyLeadNeutralTrigger } from '@/triggers/instantly/lead_neutral' +export { instantlyLeadNoShowTrigger } from '@/triggers/instantly/lead_no_show' +export { instantlyLeadNotInterestedTrigger } from '@/triggers/instantly/lead_not_interested' +export { instantlyLeadOutOfOfficeTrigger } from '@/triggers/instantly/lead_out_of_office' +export { instantlyLeadUnsubscribedTrigger } from '@/triggers/instantly/lead_unsubscribed' +export { instantlyLeadWrongPersonTrigger } from '@/triggers/instantly/lead_wrong_person' +export { instantlyLinkClickedTrigger } from '@/triggers/instantly/link_clicked' +export { instantlyReplyReceivedTrigger } from '@/triggers/instantly/reply_received' +export { instantlySupersearchEnrichmentCompletedTrigger } from '@/triggers/instantly/supersearch_enrichment_completed' +export { instantlyWebhookTrigger } from '@/triggers/instantly/webhook' diff --git a/apps/sim/triggers/instantly/lead_closed.ts b/apps/sim/triggers/instantly/lead_closed.ts new file mode 100644 index 00000000000..ee6f522fe54 --- /dev/null +++ b/apps/sim/triggers/instantly/lead_closed.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyLeadClosedTrigger = createInstantlyTrigger({ + id: 'instantly_lead_closed', + name: 'Instantly Lead Closed', + description: 'Trigger when an Instantly lead is marked closed', + eventLabel: 'Lead Closed', +}) diff --git a/apps/sim/triggers/instantly/lead_interested.ts b/apps/sim/triggers/instantly/lead_interested.ts new file mode 100644 index 00000000000..4c0791da4fb --- /dev/null +++ b/apps/sim/triggers/instantly/lead_interested.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyLeadInterestedTrigger = createInstantlyTrigger({ + id: 'instantly_lead_interested', + name: 'Instantly Lead Interested', + description: 'Trigger when an Instantly lead is marked interested', + eventLabel: 'Lead Interested', +}) diff --git a/apps/sim/triggers/instantly/lead_meeting_booked.ts b/apps/sim/triggers/instantly/lead_meeting_booked.ts new file mode 100644 index 00000000000..f74ab39b33b --- /dev/null +++ b/apps/sim/triggers/instantly/lead_meeting_booked.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyLeadMeetingBookedTrigger = createInstantlyTrigger({ + id: 'instantly_lead_meeting_booked', + name: 'Instantly Lead Meeting Booked', + description: 'Trigger when an Instantly lead books a meeting', + eventLabel: 'Lead Meeting Booked', +}) diff --git a/apps/sim/triggers/instantly/lead_meeting_completed.ts b/apps/sim/triggers/instantly/lead_meeting_completed.ts new file mode 100644 index 00000000000..470a56dd142 --- /dev/null +++ b/apps/sim/triggers/instantly/lead_meeting_completed.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyLeadMeetingCompletedTrigger = createInstantlyTrigger({ + id: 'instantly_lead_meeting_completed', + name: 'Instantly Lead Meeting Completed', + description: 'Trigger when an Instantly lead completes a meeting', + eventLabel: 'Lead Meeting Completed', +}) diff --git a/apps/sim/triggers/instantly/lead_neutral.ts b/apps/sim/triggers/instantly/lead_neutral.ts new file mode 100644 index 00000000000..0b796024b20 --- /dev/null +++ b/apps/sim/triggers/instantly/lead_neutral.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyLeadNeutralTrigger = createInstantlyTrigger({ + id: 'instantly_lead_neutral', + name: 'Instantly Lead Neutral', + description: 'Trigger when an Instantly lead is marked neutral', + eventLabel: 'Lead Neutral', +}) diff --git a/apps/sim/triggers/instantly/lead_no_show.ts b/apps/sim/triggers/instantly/lead_no_show.ts new file mode 100644 index 00000000000..a5391873810 --- /dev/null +++ b/apps/sim/triggers/instantly/lead_no_show.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyLeadNoShowTrigger = createInstantlyTrigger({ + id: 'instantly_lead_no_show', + name: 'Instantly Lead No Show', + description: 'Trigger when an Instantly lead is marked no show', + eventLabel: 'Lead No Show', +}) diff --git a/apps/sim/triggers/instantly/lead_not_interested.ts b/apps/sim/triggers/instantly/lead_not_interested.ts new file mode 100644 index 00000000000..fa5218616ec --- /dev/null +++ b/apps/sim/triggers/instantly/lead_not_interested.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyLeadNotInterestedTrigger = createInstantlyTrigger({ + id: 'instantly_lead_not_interested', + name: 'Instantly Lead Not Interested', + description: 'Trigger when an Instantly lead is marked not interested', + eventLabel: 'Lead Not Interested', +}) diff --git a/apps/sim/triggers/instantly/lead_out_of_office.ts b/apps/sim/triggers/instantly/lead_out_of_office.ts new file mode 100644 index 00000000000..e974f9cb041 --- /dev/null +++ b/apps/sim/triggers/instantly/lead_out_of_office.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyLeadOutOfOfficeTrigger = createInstantlyTrigger({ + id: 'instantly_lead_out_of_office', + name: 'Instantly Lead Out Of Office', + description: 'Trigger when an Instantly lead is out of office', + eventLabel: 'Lead Out Of Office', +}) diff --git a/apps/sim/triggers/instantly/lead_unsubscribed.ts b/apps/sim/triggers/instantly/lead_unsubscribed.ts new file mode 100644 index 00000000000..ed3132974fa --- /dev/null +++ b/apps/sim/triggers/instantly/lead_unsubscribed.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyLeadUnsubscribedTrigger = createInstantlyTrigger({ + id: 'instantly_lead_unsubscribed', + name: 'Instantly Lead Unsubscribed', + description: 'Trigger when an Instantly lead unsubscribes', + eventLabel: 'Lead Unsubscribed', +}) diff --git a/apps/sim/triggers/instantly/lead_wrong_person.ts b/apps/sim/triggers/instantly/lead_wrong_person.ts new file mode 100644 index 00000000000..44d6e79b9a6 --- /dev/null +++ b/apps/sim/triggers/instantly/lead_wrong_person.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyLeadWrongPersonTrigger = createInstantlyTrigger({ + id: 'instantly_lead_wrong_person', + name: 'Instantly Lead Wrong Person', + description: 'Trigger when an Instantly lead is marked wrong person', + eventLabel: 'Lead Wrong Person', +}) diff --git a/apps/sim/triggers/instantly/link_clicked.ts b/apps/sim/triggers/instantly/link_clicked.ts new file mode 100644 index 00000000000..a6105323aa5 --- /dev/null +++ b/apps/sim/triggers/instantly/link_clicked.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyLinkClickedTrigger = createInstantlyTrigger({ + id: 'instantly_link_clicked', + name: 'Instantly Link Clicked', + description: 'Trigger when a lead clicks a tracked Instantly link', + eventLabel: 'Link Clicked', +}) diff --git a/apps/sim/triggers/instantly/reply_received.ts b/apps/sim/triggers/instantly/reply_received.ts new file mode 100644 index 00000000000..cd62ffc80ad --- /dev/null +++ b/apps/sim/triggers/instantly/reply_received.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyReplyReceivedTrigger = createInstantlyTrigger({ + id: 'instantly_reply_received', + name: 'Instantly Reply Received', + description: 'Trigger when a lead replies to an Instantly email', + eventLabel: 'Reply Received', +}) diff --git a/apps/sim/triggers/instantly/supersearch_enrichment_completed.ts b/apps/sim/triggers/instantly/supersearch_enrichment_completed.ts new file mode 100644 index 00000000000..3a481457203 --- /dev/null +++ b/apps/sim/triggers/instantly/supersearch_enrichment_completed.ts @@ -0,0 +1,8 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlySupersearchEnrichmentCompletedTrigger = createInstantlyTrigger({ + id: 'instantly_supersearch_enrichment_completed', + name: 'Instantly Supersearch Enrichment Completed', + description: 'Trigger when Instantly completes a Supersearch enrichment', + eventLabel: 'Supersearch Enrichment Completed', +}) diff --git a/apps/sim/triggers/instantly/trigger.ts b/apps/sim/triggers/instantly/trigger.ts new file mode 100644 index 00000000000..7f82bf37c74 --- /dev/null +++ b/apps/sim/triggers/instantly/trigger.ts @@ -0,0 +1,43 @@ +import { InstantlyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildInstantlyExtraFields, + buildInstantlyOutputs, + instantlySetupInstructions, + instantlyTriggerOptions, +} from '@/triggers/instantly/utils' +import type { TriggerConfig } from '@/triggers/types' + +interface CreateInstantlyTriggerOptions { + id: string + name: string + description: string + eventLabel: string + includeDropdown?: boolean +} + +export function createInstantlyTrigger({ + id, + name, + description, + eventLabel, + includeDropdown = false, +}: CreateInstantlyTriggerOptions): TriggerConfig { + return { + id, + name, + provider: 'instantly', + description, + version: '1.0.0', + icon: InstantlyIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: id, + triggerOptions: instantlyTriggerOptions, + includeDropdown, + setupInstructions: instantlySetupInstructions(eventLabel), + extraFields: buildInstantlyExtraFields(id), + }), + outputs: buildInstantlyOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, + } +} diff --git a/apps/sim/triggers/instantly/utils.ts b/apps/sim/triggers/instantly/utils.ts new file mode 100644 index 00000000000..f666043110a --- /dev/null +++ b/apps/sim/triggers/instantly/utils.ts @@ -0,0 +1,152 @@ +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +export const INSTANTLY_TRIGGER_TO_EVENT_TYPE = { + instantly_webhook: 'all_events', + instantly_email_sent: 'email_sent', + instantly_email_opened: 'email_opened', + instantly_reply_received: 'reply_received', + instantly_auto_reply_received: 'auto_reply_received', + instantly_link_clicked: 'link_clicked', + instantly_email_bounced: 'email_bounced', + instantly_lead_unsubscribed: 'lead_unsubscribed', + instantly_account_error: 'account_error', + instantly_campaign_completed: 'campaign_completed', + instantly_lead_neutral: 'lead_neutral', + instantly_lead_interested: 'lead_interested', + instantly_lead_not_interested: 'lead_not_interested', + instantly_lead_meeting_booked: 'lead_meeting_booked', + instantly_lead_meeting_completed: 'lead_meeting_completed', + instantly_lead_closed: 'lead_closed', + instantly_lead_out_of_office: 'lead_out_of_office', + instantly_lead_wrong_person: 'lead_wrong_person', + instantly_lead_no_show: 'lead_no_show', + instantly_supersearch_enrichment_completed: 'supersearch_enrichment_completed', +} as const + +export const INSTANTLY_TRIGGER_TO_SUBSCRIPTION_EVENT_TYPE = { + ...INSTANTLY_TRIGGER_TO_EVENT_TYPE, + instantly_auto_reply_received: 'all_events', + instantly_link_clicked: 'email_link_clicked', +} as const + +export const instantlyTriggerOptions = [ + { label: 'All Events', id: 'instantly_webhook' }, + { label: 'Email Sent', id: 'instantly_email_sent' }, + { label: 'Email Opened', id: 'instantly_email_opened' }, + { label: 'Reply Received', id: 'instantly_reply_received' }, + { label: 'Auto Reply Received', id: 'instantly_auto_reply_received' }, + { label: 'Link Clicked', id: 'instantly_link_clicked' }, + { label: 'Email Bounced', id: 'instantly_email_bounced' }, + { label: 'Lead Unsubscribed', id: 'instantly_lead_unsubscribed' }, + { label: 'Account Error', id: 'instantly_account_error' }, + { label: 'Campaign Completed', id: 'instantly_campaign_completed' }, + { label: 'Lead Neutral', id: 'instantly_lead_neutral' }, + { label: 'Lead Interested', id: 'instantly_lead_interested' }, + { label: 'Lead Not Interested', id: 'instantly_lead_not_interested' }, + { label: 'Lead Meeting Booked', id: 'instantly_lead_meeting_booked' }, + { label: 'Lead Meeting Completed', id: 'instantly_lead_meeting_completed' }, + { label: 'Lead Closed', id: 'instantly_lead_closed' }, + { label: 'Lead Out Of Office', id: 'instantly_lead_out_of_office' }, + { label: 'Lead Wrong Person', id: 'instantly_lead_wrong_person' }, + { label: 'Lead No Show', id: 'instantly_lead_no_show' }, + { + label: 'Supersearch Enrichment Completed', + id: 'instantly_supersearch_enrichment_completed', + }, +] + +export function instantlySetupInstructions(eventType: string): string { + const instructions = [ + 'Enter an Instantly API Key with webhook create/delete permissions.', + 'Optionally enter a Campaign ID to receive only events for that campaign.', + `Click Save Configuration to automatically create an Instantly webhook for ${eventType}.`, + 'The webhook will be automatically deleted from Instantly when this trigger is removed.', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +export function buildInstantlyExtraFields(triggerId: string): SubBlockConfig[] { + return [ + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Instantly API key', + password: true, + required: true, + paramVisibility: 'user-only', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: 'triggerCampaignId', + title: 'Campaign ID (Optional)', + type: 'short-input', + placeholder: 'Leave empty for all campaigns', + paramVisibility: 'user-only', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +export function buildInstantlyOutputs(): Record { + return { + timestamp: { type: 'string', description: 'ISO timestamp when the event occurred' }, + eventType: { type: 'string', description: 'Instantly webhook event type' }, + workspace: { type: 'string', description: 'Instantly workspace UUID' }, + campaignId: { type: 'string', description: 'Instantly campaign UUID' }, + campaignName: { type: 'string', description: 'Instantly campaign name' }, + leadEmail: { type: 'string', description: 'Lead email address' }, + emailAccount: { type: 'string', description: 'Email account used to send the message' }, + uniboxUrl: { type: 'string', description: 'URL to view the conversation in Unibox' }, + step: { type: 'number', description: 'Campaign step number, starting at 1' }, + variant: { type: 'number', description: 'Campaign step variant number, starting at 1' }, + isFirst: { type: 'boolean', description: 'Whether this is the first event of this type' }, + emailId: { type: 'string', description: 'Email ID, usable as reply_to_uuid' }, + emailSubject: { type: 'string', description: 'Sent email subject' }, + emailText: { type: 'string', description: 'Sent email plain-text content' }, + emailHtml: { type: 'string', description: 'Sent email HTML content' }, + replyTextSnippet: { type: 'string', description: 'Short preview of the reply content' }, + replySubject: { type: 'string', description: 'Reply email subject' }, + replyText: { type: 'string', description: 'Full plain-text reply content' }, + replyHtml: { type: 'string', description: 'Full HTML reply content' }, + payload: { + type: 'json', + description: 'Full Instantly webhook payload, including any extra lead data fields', + }, + } +} + +export function getInstantlyEventTypeForTrigger(triggerId: string): string | undefined { + return INSTANTLY_TRIGGER_TO_EVENT_TYPE[triggerId as keyof typeof INSTANTLY_TRIGGER_TO_EVENT_TYPE] +} + +export function getInstantlySubscriptionEventTypeForTrigger(triggerId: string): string | undefined { + return INSTANTLY_TRIGGER_TO_SUBSCRIPTION_EVENT_TYPE[ + triggerId as keyof typeof INSTANTLY_TRIGGER_TO_SUBSCRIPTION_EVENT_TYPE + ] +} + +export function isInstantlyEventMatch(triggerId: string, body: Record): boolean { + if (triggerId === 'instantly_webhook') return true + + const expectedEventType = getInstantlyEventTypeForTrigger(triggerId) + if (!expectedEventType) return false + + const actualEventType = body.event_type + if (typeof actualEventType !== 'string') return false + + if (triggerId === 'instantly_link_clicked') { + return actualEventType === 'link_clicked' || actualEventType === 'email_link_clicked' + } + + return actualEventType === expectedEventType +} diff --git a/apps/sim/triggers/instantly/webhook.ts b/apps/sim/triggers/instantly/webhook.ts new file mode 100644 index 00000000000..904f2192ac0 --- /dev/null +++ b/apps/sim/triggers/instantly/webhook.ts @@ -0,0 +1,9 @@ +import { createInstantlyTrigger } from '@/triggers/instantly/trigger' + +export const instantlyWebhookTrigger = createInstantlyTrigger({ + id: 'instantly_webhook', + name: 'Instantly Webhook', + description: 'Trigger workflow on any Instantly webhook event', + eventLabel: 'All Events', + includeDropdown: true, +}) diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 43a5af5dedf..0de5587cb5b 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -147,6 +147,28 @@ import { } from '@/triggers/greenhouse' import { hubspotPollingTrigger } from '@/triggers/hubspot' import { imapPollingTrigger } from '@/triggers/imap' +import { + instantlyAccountErrorTrigger, + instantlyAutoReplyReceivedTrigger, + instantlyCampaignCompletedTrigger, + instantlyEmailBouncedTrigger, + instantlyEmailOpenedTrigger, + instantlyEmailSentTrigger, + instantlyLeadClosedTrigger, + instantlyLeadInterestedTrigger, + instantlyLeadMeetingBookedTrigger, + instantlyLeadMeetingCompletedTrigger, + instantlyLeadNeutralTrigger, + instantlyLeadNoShowTrigger, + instantlyLeadNotInterestedTrigger, + instantlyLeadOutOfOfficeTrigger, + instantlyLeadUnsubscribedTrigger, + instantlyLeadWrongPersonTrigger, + instantlyLinkClickedTrigger, + instantlyReplyReceivedTrigger, + instantlySupersearchEnrichmentCompletedTrigger, + instantlyWebhookTrigger, +} from '@/triggers/instantly' import { intercomContactCreatedTrigger, intercomConversationClosedTrigger, @@ -563,6 +585,26 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { intercom_contact_created: intercomContactCreatedTrigger, intercom_user_created: intercomUserCreatedTrigger, intercom_webhook: intercomWebhookTrigger, + instantly_webhook: instantlyWebhookTrigger, + instantly_email_sent: instantlyEmailSentTrigger, + instantly_email_opened: instantlyEmailOpenedTrigger, + instantly_reply_received: instantlyReplyReceivedTrigger, + instantly_auto_reply_received: instantlyAutoReplyReceivedTrigger, + instantly_link_clicked: instantlyLinkClickedTrigger, + instantly_email_bounced: instantlyEmailBouncedTrigger, + instantly_lead_unsubscribed: instantlyLeadUnsubscribedTrigger, + instantly_account_error: instantlyAccountErrorTrigger, + instantly_campaign_completed: instantlyCampaignCompletedTrigger, + instantly_lead_neutral: instantlyLeadNeutralTrigger, + instantly_lead_interested: instantlyLeadInterestedTrigger, + instantly_lead_not_interested: instantlyLeadNotInterestedTrigger, + instantly_lead_meeting_booked: instantlyLeadMeetingBookedTrigger, + instantly_lead_meeting_completed: instantlyLeadMeetingCompletedTrigger, + instantly_lead_closed: instantlyLeadClosedTrigger, + instantly_lead_out_of_office: instantlyLeadOutOfOfficeTrigger, + instantly_lead_wrong_person: instantlyLeadWrongPersonTrigger, + instantly_lead_no_show: instantlyLeadNoShowTrigger, + instantly_supersearch_enrichment_completed: instantlySupersearchEnrichmentCompletedTrigger, zoom_meeting_started: zoomMeetingStartedTrigger, zoom_meeting_ended: zoomMeetingEndedTrigger, zoom_participant_joined: zoomParticipantJoinedTrigger, From 2a455ff69e94b2bdf642a167f560b31d9263632e Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 27 May 2026 15:26:14 -0700 Subject: [PATCH 2/5] change bg color for icon --- apps/docs/content/docs/en/tools/instantly.mdx | 2 +- apps/sim/app/(landing)/integrations/data/integrations.json | 2 +- apps/sim/blocks/blocks/instantly.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/docs/content/docs/en/tools/instantly.mdx b/apps/docs/content/docs/en/tools/instantly.mdx index 354e86636ce..811fefde4ca 100644 --- a/apps/docs/content/docs/en/tools/instantly.mdx +++ b/apps/docs/content/docs/en/tools/instantly.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" ## Usage Instructions diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 6d16b77e08e..0832536e1de 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -7034,7 +7034,7 @@ "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.", - "bgColor": "#FF6B35", + "bgColor": "#FFFFFF", "iconName": "InstantlyIcon", "docsUrl": "https://docs.sim.ai/tools/instantly", "operations": [ diff --git a/apps/sim/blocks/blocks/instantly.ts b/apps/sim/blocks/blocks/instantly.ts index cde31692378..29e51db6200 100644 --- a/apps/sim/blocks/blocks/instantly.ts +++ b/apps/sim/blocks/blocks/instantly.ts @@ -55,7 +55,7 @@ export const InstantlyBlock: BlockConfig = { category: 'tools', integrationType: IntegrationType.Email, tags: ['sales-engagement', 'email-marketing', 'automation'], - bgColor: '#FF6B35', + bgColor: '#FFFFFF', icon: InstantlyIcon, authMode: AuthMode.ApiKey, subBlocks: [ From 81e04d4402f695d6a78c5d600c347e1fd311f886 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 27 May 2026 16:05:42 -0700 Subject: [PATCH 3/5] address comments --- apps/sim/blocks/blocks/instantly.ts | 441 ++++++++++++++++-- apps/sim/blocks/registry.ts | 2 +- apps/sim/lib/webhooks/providers/instantly.ts | 28 +- apps/sim/tools/instantly/activate_campaign.ts | 5 +- apps/sim/tools/instantly/create_campaign.ts | 18 +- apps/sim/tools/instantly/create_lead.ts | 9 +- apps/sim/tools/instantly/create_lead_list.ts | 3 +- apps/sim/tools/instantly/delete_leads.ts | 3 +- apps/sim/tools/instantly/get_lead.ts | 5 +- apps/sim/tools/instantly/list_campaigns.ts | 3 +- apps/sim/tools/instantly/list_emails.ts | 14 +- apps/sim/tools/instantly/list_lead_lists.ts | 3 +- apps/sim/tools/instantly/list_leads.ts | 22 +- apps/sim/tools/instantly/patch_campaign.ts | 20 +- apps/sim/tools/instantly/reply_to_email.ts | 3 +- apps/sim/tools/instantly/types.ts | 19 +- .../instantly/update_lead_interest_status.ts | 18 +- apps/sim/tools/instantly/utils.ts | 29 +- apps/sim/triggers/instantly/utils.ts | 2 +- 19 files changed, 513 insertions(+), 134 deletions(-) diff --git a/apps/sim/blocks/blocks/instantly.ts b/apps/sim/blocks/blocks/instantly.ts index 29e51db6200..695b3782a87 100644 --- a/apps/sim/blocks/blocks/instantly.ts +++ b/apps/sim/blocks/blocks/instantly.ts @@ -87,6 +87,7 @@ export const InstantlyBlock: BlockConfig = { placeholder: 'Enter your Instantly API key', password: true, required: true, + paramVisibility: 'user-only', }, { id: 'leadId', @@ -121,7 +122,11 @@ export const InstantlyBlock: BlockConfig = { title: 'Lead Email', type: 'short-input', placeholder: 'jane@example.com', - required: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + required: { + field: 'operation', + value: [...LEAD_CREATE_OPERATIONS], + and: { field: 'leadDestination', value: 'campaign' }, + }, condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, }, { @@ -186,7 +191,7 @@ export const InstantlyBlock: BlockConfig = { wandConfig: { enabled: true, prompt: - 'Generate a JSON object of Instantly custom variables. Values must be strings, numbers, booleans, or null. Return ONLY the JSON object.', + 'Generate a JSON object of Instantly custom variables. Values must be strings, numbers, booleans, or null. Return ONLY the JSON object - no explanations, no extra text.', generationType: 'json-object', }, mode: 'advanced', @@ -230,6 +235,64 @@ export const InstantlyBlock: BlockConfig = { condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, mode: 'advanced', }, + { + id: 'verifyLeadsForLeadFinder', + title: 'Verify Leads For Lead Finder', + type: 'dropdown', + options: [ + { label: 'Unspecified', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'verifyLeadsOnImport', + title: 'Verify Leads On Import', + type: 'dropdown', + options: [ + { label: 'Unspecified', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'leadInterestStatus', + title: 'Lead Interest Status', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'potentialLeadValue', + title: 'Potential Lead Value', + type: 'short-input', + placeholder: 'High', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'assignedTo', + title: 'Assigned To', + type: 'short-input', + placeholder: 'Organization user UUID', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'blocklistId', + title: 'Blocklist ID', + type: 'short-input', + placeholder: 'Blocklist UUID', + condition: { field: 'operation', value: [...LEAD_CREATE_OPERATIONS] }, + mode: 'advanced', + }, { id: 'search', title: 'Search', @@ -300,6 +363,14 @@ export const InstantlyBlock: BlockConfig = { condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, mode: 'advanced', }, + { + id: 'excludedLeadIds', + title: 'Excluded Lead IDs', + type: 'long-input', + placeholder: 'lead-id-1, lead-id-2', + condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, + mode: 'advanced', + }, { id: 'contacts', title: 'Contacts', @@ -334,6 +405,64 @@ export const InstantlyBlock: BlockConfig = { condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, mode: 'advanced', }, + { + id: 'organizationUserIds', + title: 'Organization User IDs', + type: 'long-input', + placeholder: 'user-id-1, user-id-2', + condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'smartViewId', + title: 'Smart View ID', + type: 'short-input', + placeholder: 'Smart view UUID', + condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'websiteVisitor', + title: 'Website Visitor', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'distinctContacts', + title: 'Distinct Contacts', + type: 'dropdown', + options: [ + { label: 'Unspecified', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'enrichmentStatus', + title: 'Enrichment Status', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'esgCode', + title: 'ESG Code', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: [...LEAD_LIST_OPERATIONS] }, + mode: 'advanced', + }, { id: 'deleteSource', title: 'Delete From', @@ -390,8 +519,7 @@ export const InstantlyBlock: BlockConfig = { id: 'interestValue', title: 'Interest Value', type: 'short-input', - placeholder: '1', - required: { field: 'operation', value: [...LEAD_INTEREST_OPERATIONS] }, + placeholder: '1 or null', condition: { field: 'operation', value: [...LEAD_INTEREST_OPERATIONS] }, }, { @@ -407,6 +535,14 @@ export const InstantlyBlock: BlockConfig = { condition: { field: 'operation', value: [...LEAD_INTEREST_OPERATIONS] }, mode: 'advanced', }, + { + id: 'aiInterestValue', + title: 'AI Interest Value', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: [...LEAD_INTEREST_OPERATIONS] }, + mode: 'advanced', + }, { id: 'campaignName', title: 'Campaign Name', @@ -426,7 +562,7 @@ export const InstantlyBlock: BlockConfig = { wandConfig: { enabled: true, prompt: - 'Generate an Instantly API V2 campaign_schedule JSON object with schedules containing name, timing.from, timing.to, days, and timezone. Return ONLY the JSON object.', + 'Generate an Instantly API V2 campaign_schedule JSON object with schedules containing name, timing.from, timing.to, days, and timezone. Return ONLY the JSON object - no explanations, no extra text.', generationType: 'json-object', }, }, @@ -440,7 +576,7 @@ export const InstantlyBlock: BlockConfig = { wandConfig: { enabled: true, prompt: - 'Generate an Instantly API V2 sequences JSON array. Use one sequence with steps; each step must have type "email", delay, and variants with subject and body. Return ONLY the JSON array.', + 'Generate an Instantly API V2 sequences JSON array. Use one sequence with steps; each step must have type "email", delay, and variants with subject and body. Return ONLY the JSON array - no explanations, no extra text.', generationType: 'json-object', }, mode: 'advanced', @@ -495,6 +631,48 @@ export const InstantlyBlock: BlockConfig = { condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, mode: 'advanced', }, + { + id: 'positiveLeadValue', + title: 'Positive Lead Value', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'emailGap', + title: 'Email Gap', + type: 'short-input', + placeholder: '10', + condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'linkTracking', + title: 'Link Tracking', + type: 'dropdown', + options: [ + { label: 'Unspecified', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'textOnly', + title: 'Text Only', + type: 'dropdown', + options: [ + { label: 'Unspecified', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...CAMPAIGN_MUTATION_OPERATIONS] }, + mode: 'advanced', + }, { id: 'tagIds', title: 'Tag IDs', @@ -503,6 +681,14 @@ export const InstantlyBlock: BlockConfig = { condition: { field: 'operation', value: [...CAMPAIGN_LIST_OPERATIONS] }, mode: 'advanced', }, + { + id: 'aiSalesAgentId', + title: 'AI Sales Agent ID', + type: 'short-input', + placeholder: 'AI Sales Agent UUID', + condition: { field: 'operation', value: [...CAMPAIGN_LIST_OPERATIONS] }, + mode: 'advanced', + }, { id: 'campaignStatus', title: 'Campaign Status', @@ -529,6 +715,35 @@ export const InstantlyBlock: BlockConfig = { placeholder: 'lead@example.com or thread:uuid', condition: { field: 'operation', value: [...EMAIL_LIST_OPERATIONS] }, }, + { + id: 'emailStatus', + title: 'Email Status', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: [...EMAIL_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'emailLead', + title: 'Lead Email Filter', + type: 'short-input', + placeholder: 'lead@example.com', + condition: { field: 'operation', value: [...EMAIL_LIST_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'emailIsUnread', + title: 'Unread', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: [...EMAIL_LIST_OPERATIONS] }, + mode: 'advanced', + }, { id: 'replyToUuid', title: 'Reply To Email ID', @@ -561,6 +776,22 @@ export const InstantlyBlock: BlockConfig = { condition: { field: 'operation', value: [...EMAIL_REPLY_OPERATIONS] }, mode: 'advanced', }, + { + id: 'ccRecipients', + title: 'CC Recipients', + type: 'short-input', + placeholder: 'cc@example.com', + condition: { field: 'operation', value: [...EMAIL_REPLY_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'bccRecipients', + title: 'BCC Recipients', + type: 'short-input', + placeholder: 'bcc@example.com', + condition: { field: 'operation', value: [...EMAIL_REPLY_OPERATIONS] }, + mode: 'advanced', + }, { id: 'leadListName', title: 'Lead List Name', @@ -631,40 +862,55 @@ export const InstantlyBlock: BlockConfig = { tool: (params) => `instantly_${params.operation}`, params: (params) => ({ campaign: - params.leadDestination === 'campaign' - ? emptyToUndefined(params.leadDestinationId) - : undefined, + params.operation === 'list_leads' + ? optionalIdParam(params.campaignId) + : params.leadDestination === 'campaign' + ? optionalIdParam(params.leadDestinationId) + : undefined, list_id: params.operation === 'delete_leads' && params.deleteSource === 'list' - ? emptyToUndefined(params.deleteSourceId) + ? optionalIdParam(params.deleteSourceId) : params.leadDestination === 'list' - ? emptyToUndefined(params.leadDestinationId) - : emptyToUndefined(params.listId), + ? optionalIdParam(params.leadDestinationId) + : optionalIdParam(params.listId), campaign_id: params.operation === 'delete_leads' ? params.deleteSource === 'campaign' - ? emptyToUndefined(params.deleteSourceId) + ? optionalIdParam(params.deleteSourceId) : undefined - : emptyToUndefined(params.campaignId), + : optionalIdParam(params.campaignId), leadId: params.leadId, - email: params.email, - first_name: params.firstName, - last_name: params.lastName, - company_name: params.companyName, - job_title: params.jobTitle, - phone: params.phone, - website: params.website, - personalization: params.personalization, + email: emptyToUndefined(params.email), + first_name: emptyToUndefined(params.firstName), + last_name: emptyToUndefined(params.lastName), + company_name: emptyToUndefined(params.companyName), + job_title: emptyToUndefined(params.jobTitle), + phone: emptyToUndefined(params.phone), + website: emptyToUndefined(params.website), + personalization: emptyToUndefined(params.personalization), custom_variables: parseJsonObject(params.customVariables), + lt_interest_status: toNumberParam(params.leadInterestStatus), + pl_value_lead: emptyToUndefined(params.potentialLeadValue), + assigned_to: optionalIdParam(params.assignedTo), + blocklist_id: optionalIdParam(params.blocklistId), skip_if_in_workspace: toBooleanParam(params.skipIfInWorkspace), skip_if_in_campaign: toBooleanParam(params.skipIfInCampaign), skip_if_in_list: toBooleanParam(params.skipIfInList), + verify_leads_for_lead_finder: toBooleanParam(params.verifyLeadsForLeadFinder), + verify_leads_on_import: toBooleanParam(params.verifyLeadsOnImport), filter: emptyToUndefined(params.leadFilter), ids: params.operation === 'delete_leads' ? parseStringList(params.deleteLeadIds) : parseStringList(params.leadIds), + excluded_ids: parseStringList(params.excludedLeadIds), contacts: parseStringList(params.contacts), + organization_user_ids: parseStringList(params.organizationUserIds), + smart_view_id: optionalIdParam(params.smartViewId), + is_website_visitor: toBooleanParam(params.websiteVisitor), + distinct_contacts: toBooleanParam(params.distinctContacts), + enrichment_status: toNumberParam(params.enrichmentStatus), + esg_code: emptyToUndefined(params.esgCode), in_campaign: toBooleanParam(params.inCampaign), in_list: toBooleanParam(params.inList), status: @@ -675,13 +921,17 @@ export const InstantlyBlock: BlockConfig = { params.operation === 'delete_leads' ? toNumberParam(params.deleteLimit) : toNumberParam(params.limit), - starting_after: params.startingAfter, - lead_email: params.leadEmail, - interest_value: toNumberParam(params.interestValue), + starting_after: emptyToUndefined(params.startingAfter), + lead_email: emptyToUndefined(params.leadEmail), + interest_value: + params.operation === 'update_lead_interest_status' + ? toNullableNumberParam(params.interestValue, true) + : undefined, + ai_interest_value: toNumberParam(params.aiInterestValue), disable_auto_interest: toBooleanParam(params.disableAutoInterest), name: params.operation === 'create_lead_list' - ? params.leadListName + ? emptyToUndefined(params.leadListName) : emptyToUndefined(params.campaignName), campaign_schedule: parseJsonObject(params.campaignSchedule), sequences: parseJsonArray(params.sequences), @@ -690,20 +940,30 @@ export const InstantlyBlock: BlockConfig = { daily_max_leads: toNumberParam(params.dailyMaxLeads), open_tracking: toBooleanParam(params.openTracking), stop_on_reply: toBooleanParam(params.stopOnReply), + pl_value: toNumberParam(params.positiveLeadValue), + email_gap: toNumberParam(params.emailGap), + link_tracking: toBooleanParam(params.linkTracking), + text_only: toBooleanParam(params.textOnly), tag_ids: emptyToUndefined(params.tagIds), + ai_sales_agent_id: optionalIdParam(params.aiSalesAgentId), search: params.operation === 'list_emails' ? emptyToUndefined(params.emailSearch) : emptyToUndefined(params.search), - eaccount: params.emailAccount, - reply_to_uuid: params.replyToUuid, - subject: params.subject, + eaccount: emptyToUndefined(params.emailAccount), + i_status: toNumberParam(params.emailStatus), + lead: emptyToUndefined(params.emailLead), + is_unread: toBooleanParam(params.emailIsUnread), + reply_to_uuid: emptyToUndefined(params.replyToUuid), + subject: emptyToUndefined(params.subject), body: { - text: params.bodyText, + text: emptyToUndefined(params.bodyText), html: emptyToUndefined(params.bodyHtml), }, + cc_address_email_list: emptyToUndefined(params.ccRecipients), + bcc_address_email_list: emptyToUndefined(params.bccRecipients), has_enrichment_task: toBooleanParam(params.hasEnrichmentTask), - owned_by: emptyToUndefined(params.ownedBy), + owned_by: optionalIdParam(params.ownedBy), }), }, }, @@ -717,34 +977,104 @@ export const InstantlyBlock: BlockConfig = { firstName: { type: 'string', description: 'Lead first name' }, lastName: { type: 'string', description: 'Lead last name' }, companyName: { type: 'string', description: 'Company name' }, + leadInterestStatus: { type: 'number', description: 'Lead interest status value' }, + potentialLeadValue: { type: 'string', description: 'Potential value of the lead' }, + assignedTo: { type: 'string', description: 'Organization user ID assigned to the lead' }, + blocklistId: { type: 'string', description: 'Blocklist ID' }, + verifyLeadsForLeadFinder: { + type: 'boolean', + description: 'Whether to verify leads imported from Lead Finder', + }, + verifyLeadsOnImport: { type: 'boolean', description: 'Whether to verify leads on import' }, search: { type: 'string', description: 'Search query' }, + excludedLeadIds: { type: 'string', description: 'Lead IDs to exclude' }, + contacts: { type: 'string', description: 'Lead email addresses to include' }, + organizationUserIds: { type: 'string', description: 'Organization user IDs to filter leads' }, + smartViewId: { type: 'string', description: 'Smart view ID to filter leads' }, + websiteVisitor: { type: 'boolean', description: 'Whether the lead is a website visitor' }, + distinctContacts: { type: 'boolean', description: 'Whether to return distinct contacts' }, + enrichmentStatus: { type: 'number', description: 'Enrichment status filter' }, + esgCode: { type: 'string', description: 'Email security gateway code filter' }, campaignId: { type: 'string', description: 'Campaign ID' }, listId: { type: 'string', description: 'Lead list ID' }, + leadIds: { type: 'string', description: 'Lead IDs' }, + inCampaign: { type: 'boolean', description: 'Whether the lead is in a campaign' }, + inList: { type: 'boolean', description: 'Whether the lead is in a list' }, deleteSource: { type: 'string', description: 'Delete source type' }, deleteSourceId: { type: 'string', description: 'Delete source ID' }, + deleteStatus: { type: 'number', description: 'Delete status filter' }, + deleteLeadIds: { type: 'string', description: 'Lead IDs to delete' }, + deleteLimit: { type: 'number', description: 'Maximum number of leads to delete' }, leadEmail: { type: 'string', description: 'Lead email for interest update' }, interestValue: { type: 'number', description: 'Interest status value' }, + disableAutoInterest: { type: 'boolean', description: 'Whether to disable auto interest' }, + aiInterestValue: { type: 'number', description: 'AI interest value' }, campaignName: { type: 'string', description: 'Campaign name' }, campaignSchedule: { type: 'json', description: 'Campaign schedule object' }, sequences: { type: 'array', description: 'Campaign sequences' }, + emailList: { type: 'string', description: 'Sending email accounts' }, + dailyLimit: { type: 'number', description: 'Daily sending limit' }, + dailyMaxLeads: { type: 'number', description: 'Daily maximum new leads' }, + openTracking: { type: 'boolean', description: 'Whether to track opens' }, + stopOnReply: { type: 'boolean', description: 'Whether to stop on replies' }, + positiveLeadValue: { type: 'number', description: 'Value of every positive lead' }, + emailGap: { type: 'number', description: 'Gap between emails in minutes' }, + linkTracking: { type: 'boolean', description: 'Whether to track links' }, + textOnly: { type: 'boolean', description: 'Whether the campaign is text only' }, + tagIds: { type: 'string', description: 'Campaign tag IDs' }, + aiSalesAgentId: { type: 'string', description: 'AI Sales Agent ID' }, + campaignStatus: { type: 'number', description: 'Campaign status filter' }, emailAccount: { type: 'string', description: 'Email account' }, emailSearch: { type: 'string', description: 'Email search query' }, + emailStatus: { type: 'number', description: 'Email interest status filter' }, + emailLead: { type: 'string', description: 'Lead email filter' }, + emailIsUnread: { type: 'boolean', description: 'Whether the email is unread' }, replyToUuid: { type: 'string', description: 'Email ID to reply to' }, subject: { type: 'string', description: 'Reply subject' }, bodyText: { type: 'string', description: 'Reply body text' }, + bodyHtml: { type: 'string', description: 'Reply body HTML' }, + ccRecipients: { type: 'string', description: 'CC email recipients' }, + bccRecipients: { type: 'string', description: 'BCC email recipients' }, leadListName: { type: 'string', description: 'Lead list name' }, + hasEnrichmentTask: { type: 'boolean', description: 'Whether the lead list has enrichment' }, + ownedBy: { type: 'string', description: 'Owner user ID' }, limit: { type: 'number', description: 'Page size' }, startingAfter: { type: 'string', description: 'Pagination cursor' }, }, outputs: { - leads: { type: 'array', description: 'List of leads' }, - lead: { type: 'json', description: 'Lead details' }, - campaigns: { type: 'array', description: 'List of campaigns' }, - campaign: { type: 'json', description: 'Campaign details' }, - emails: { type: 'array', description: 'List of emails' }, - email: { type: 'json', description: 'Email details' }, - lead_lists: { type: 'array', description: 'List of lead lists' }, - lead_list: { type: 'json', description: 'Lead list details' }, + leads: { + type: 'array', + description: 'List of leads (id, email, first_name, last_name, campaign, status)', + }, + lead: { + type: 'json', + description: + 'Lead details (id, email, first_name, last_name, company_name, job_title, campaign, status, payload)', + }, + campaigns: { type: 'array', description: 'List of campaigns (id, name, status, daily_limit)' }, + campaign: { + type: 'json', + description: + 'Campaign details (id, name, status, daily_limit, daily_max_leads, open_tracking)', + }, + emails: { + type: 'array', + description: 'List of emails (id, subject, from_address_email, lead, thread_id)', + }, + email: { + type: 'json', + description: + 'Email details (id, subject, from_address_email, to_address_email_list, thread_id, content_preview)', + }, + lead_lists: { + type: 'array', + description: 'List of lead lists (id, name, has_enrichment_task, timestamp_created)', + }, + lead_list: { + type: 'json', + description: + 'Lead list details (id, organization_id, has_enrichment_task, owned_by, name, timestamp_created)', + }, count: { type: 'number', description: 'Returned or affected record count' }, next_starting_after: { type: 'string', description: 'Cursor for the next page' }, id: { type: 'string', description: 'Record ID' }, @@ -786,16 +1116,21 @@ export const InstantlyBlock: BlockConfig = { function parseStringList(value: unknown): string[] | undefined { if (Array.isArray(value)) { - const strings = value.filter((item): item is string => typeof item === 'string' && item !== '') + const strings = value + .filter((item): item is string => typeof item === 'string') + .map((item) => item.trim()) + .filter((item) => item !== '' && item !== '-') return strings.length > 0 ? strings : undefined } - if (typeof value !== 'string' || value.trim() === '') return undefined + if (typeof value !== 'string') return undefined + const trimmed = value.trim() + if (trimmed === '' || trimmed === '-') return undefined - const strings = value + const strings = trimmed .split(/[\s,]+/) .map((item) => item.trim()) - .filter(Boolean) + .filter((item) => item !== '' && item !== '-') return strings.length > 0 ? strings : undefined } @@ -832,6 +1167,15 @@ function toNumberParam(value: unknown): number | undefined { return Number.isFinite(parsed) ? parsed : undefined } +function toNullableNumberParam(value: unknown, emptyAsNull = false): number | null | undefined { + if (value === null) return null + if (emptyAsNull && value === undefined) return null + if (emptyAsNull && typeof value === 'string' && value.trim() === '-') return null + if (typeof value === 'string' && value.trim().toLowerCase() === 'null') return null + if (emptyAsNull && typeof value === 'string' && value.trim() === '') return null + return toNumberParam(value) +} + function toBooleanParam(value: unknown): boolean | undefined { if (typeof value === 'boolean') return value if (typeof value !== 'string' || value.trim() === '') return undefined @@ -841,7 +1185,16 @@ function toBooleanParam(value: unknown): boolean | undefined { } function emptyToUndefined(value: unknown): unknown { - return value === '' ? undefined : value + if (typeof value !== 'string') return value + const trimmed = value.trim() + return trimmed === '' || trimmed === '-' ? undefined : trimmed +} + +function optionalIdParam(value: unknown): string | undefined { + if (typeof value !== 'string') return undefined + const trimmed = value.trim() + if (trimmed === '' || trimmed === '-') return undefined + return trimmed } function isPlainObject(value: unknown): value is Record { diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 637c1ce5926..79e8191546f 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -365,9 +365,9 @@ export const registry: Record = { incidentio: IncidentioBlock, infisical: InfisicalBlock, input_trigger: InputTriggerBlock, + instantly: InstantlyBlock, intercom: IntercomBlock, intercom_v2: IntercomV2Block, - instantly: InstantlyBlock, jina: JinaBlock, jira: JiraBlock, jira_service_management: JiraServiceManagementBlock, diff --git a/apps/sim/lib/webhooks/providers/instantly.ts b/apps/sim/lib/webhooks/providers/instantly.ts index a68b4e9d3e2..17bd554e734 100644 --- a/apps/sim/lib/webhooks/providers/instantly.ts +++ b/apps/sim/lib/webhooks/providers/instantly.ts @@ -22,7 +22,10 @@ const SIM_WEBHOOK_TOKEN_HEADER = 'x-sim-webhook-token' export const instantlyHandler: WebhookProviderHandler = { verifyAuth({ request, requestId, providerConfig }: AuthContext): NextResponse | null { const secretToken = providerConfig.secretToken as string | undefined - if (!secretToken) return null + if (!secretToken) { + logger.warn(`[${requestId}] Instantly webhook secret token is missing`) + return new NextResponse('Unauthorized', { status: 401 }) + } if (!verifyTokenAuth(request, secretToken, SIM_WEBHOOK_TOKEN_HEADER)) { logger.warn(`[${requestId}] Unauthorized Instantly webhook request`) @@ -85,9 +88,9 @@ export const instantlyHandler: WebhookProviderHandler = { async createSubscription(ctx: SubscriptionContext): Promise { const { webhook, requestId } = ctx const providerConfig = getProviderConfig(webhook) - const apiKey = providerConfig.apiKey as string | undefined + const apiKey = providerConfig.triggerApiKey as string | undefined const triggerId = providerConfig.triggerId as string | undefined - const campaignId = providerConfig.triggerCampaignId as string | undefined + const campaignId = optionalId(providerConfig.triggerCampaignId) if (!apiKey?.trim()) { throw new Error('Instantly API Key is required.') @@ -119,8 +122,8 @@ export const instantlyHandler: WebhookProviderHandler = { }, } - if (campaignId?.trim()) { - requestBody.campaign = campaignId.trim() + if (campaignId) { + requestBody.campaign = campaignId } logger.info(`[${requestId}] Creating Instantly webhook`, { @@ -133,7 +136,7 @@ export const instantlyHandler: WebhookProviderHandler = { const response = await fetch(instantlyUrl('/api/v2/webhooks'), { method: 'POST', headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${apiKey.trim()}`, 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), @@ -179,7 +182,7 @@ export const instantlyHandler: WebhookProviderHandler = { try { const providerConfig = getProviderConfig(webhook) - const apiKey = providerConfig.apiKey as string | undefined + const apiKey = providerConfig.triggerApiKey as string | undefined const externalId = providerConfig.externalId as string | undefined if (!apiKey?.trim() || !externalId?.trim()) { @@ -193,11 +196,11 @@ export const instantlyHandler: WebhookProviderHandler = { } const response = await fetch( - instantlyUrl(`/api/v2/webhooks/${encodeURIComponent(externalId)}`), + instantlyUrl(`/api/v2/webhooks/${encodeURIComponent(externalId.trim())}`), { method: 'DELETE', headers: { - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${apiKey.trim()}`, }, } ) @@ -254,6 +257,13 @@ function toBooleanOrNull(value: unknown): boolean | null { return typeof value === 'boolean' ? value : null } +function optionalId(value: unknown): string | undefined { + if (typeof value !== 'string') return undefined + const trimmed = value.trim() + if (trimmed === '' || trimmed === '-') return undefined + return trimmed +} + function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === 'object' && !Array.isArray(value) } diff --git a/apps/sim/tools/instantly/activate_campaign.ts b/apps/sim/tools/instantly/activate_campaign.ts index 718af46d661..b9755b082ea 100644 --- a/apps/sim/tools/instantly/activate_campaign.ts +++ b/apps/sim/tools/instantly/activate_campaign.ts @@ -8,6 +8,7 @@ import { instantlyHeaders, instantlyUrl, mapCampaign, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -29,12 +30,12 @@ export const activateCampaignTool: ToolConfig< }, }, request: { - url: (params) => instantlyUrl(`/api/v2/campaigns/${params.campaignId}/activate`), + url: (params) => instantlyUrl(`/api/v2/campaigns/${params.campaignId.trim()}/activate`), method: 'POST', headers: instantlyHeaders, }, transformResponse: async (response) => { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const campaign = mapCampaign(data) return { diff --git a/apps/sim/tools/instantly/create_campaign.ts b/apps/sim/tools/instantly/create_campaign.ts index 094dd522325..d900d912c42 100644 --- a/apps/sim/tools/instantly/create_campaign.ts +++ b/apps/sim/tools/instantly/create_campaign.ts @@ -9,6 +9,7 @@ import { instantlyHeaders, instantlyUrl, mapCampaign, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -107,33 +108,18 @@ export const createCampaignTool: ToolConfig< campaign_schedule: params.campaign_schedule, sequences: params.sequences, pl_value: params.pl_value, - is_evergreen: params.is_evergreen, email_gap: params.email_gap, - random_wait_max: params.random_wait_max, text_only: params.text_only, - first_email_text_only: params.first_email_text_only, email_list: params.email_list, daily_limit: params.daily_limit, stop_on_reply: params.stop_on_reply, - email_tag_list: params.email_tag_list, link_tracking: params.link_tracking, open_tracking: params.open_tracking, - stop_on_auto_reply: params.stop_on_auto_reply, daily_max_leads: params.daily_max_leads, - prioritize_new_leads: params.prioritize_new_leads, - match_lead_esp: params.match_lead_esp, - stop_for_company: params.stop_for_company, - insert_unsubscribe_header: params.insert_unsubscribe_header, - allow_risky_contacts: params.allow_risky_contacts, - disable_bounce_protect: params.disable_bounce_protect, - cc_list: params.cc_list, - bcc_list: params.bcc_list, - owned_by: params.owned_by, - ai_sdr_id: params.ai_sdr_id, }), }, transformResponse: async (response) => { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const campaign = mapCampaign(data) return { diff --git a/apps/sim/tools/instantly/create_lead.ts b/apps/sim/tools/instantly/create_lead.ts index a6c57db3956..21ed22974f7 100644 --- a/apps/sim/tools/instantly/create_lead.ts +++ b/apps/sim/tools/instantly/create_lead.ts @@ -6,6 +6,7 @@ import { instantlyUrl, leadOutputs, mapLead, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -118,6 +119,12 @@ export const createLeadTool: ToolConfig { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const lead = mapLead(data) return { diff --git a/apps/sim/tools/instantly/create_lead_list.ts b/apps/sim/tools/instantly/create_lead_list.ts index 6a5c48ae20e..288b61ca50a 100644 --- a/apps/sim/tools/instantly/create_lead_list.ts +++ b/apps/sim/tools/instantly/create_lead_list.ts @@ -9,6 +9,7 @@ import { instantlyUrl, leadListOutputs, mapLeadList, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -53,7 +54,7 @@ export const createLeadListTool: ToolConfig< }), }, transformResponse: async (response) => { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const leadList = mapLeadList(data) return { diff --git a/apps/sim/tools/instantly/delete_leads.ts b/apps/sim/tools/instantly/delete_leads.ts index 8933510336f..8e313a2df8e 100644 --- a/apps/sim/tools/instantly/delete_leads.ts +++ b/apps/sim/tools/instantly/delete_leads.ts @@ -8,6 +8,7 @@ import { instantlyBaseParamFields, instantlyHeaders, instantlyUrl, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -65,7 +66,7 @@ export const deleteLeadsTool: ToolConfig { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const result = asRecord(data) return { diff --git a/apps/sim/tools/instantly/get_lead.ts b/apps/sim/tools/instantly/get_lead.ts index e012c40d341..1f7239d68ff 100644 --- a/apps/sim/tools/instantly/get_lead.ts +++ b/apps/sim/tools/instantly/get_lead.ts @@ -5,6 +5,7 @@ import { instantlyUrl, leadOutputs, mapLead, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -23,12 +24,12 @@ export const getLeadTool: ToolConfig instantlyUrl(`/api/v2/leads/${params.leadId}`), + url: (params) => instantlyUrl(`/api/v2/leads/${params.leadId.trim()}`), method: 'GET', headers: instantlyHeaders, }, transformResponse: async (response) => { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const lead = mapLead(data) return { diff --git a/apps/sim/tools/instantly/list_campaigns.ts b/apps/sim/tools/instantly/list_campaigns.ts index c93b9885625..fa51ca23b96 100644 --- a/apps/sim/tools/instantly/list_campaigns.ts +++ b/apps/sim/tools/instantly/list_campaigns.ts @@ -10,6 +10,7 @@ import { instantlyHeaders, instantlyUrl, mapCampaign, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -74,7 +75,7 @@ export const listCampaignsTool: ToolConfig< headers: instantlyHeaders, }, transformResponse: async (response) => { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const campaigns = getItems(data).map(mapCampaign) return { diff --git a/apps/sim/tools/instantly/list_emails.ts b/apps/sim/tools/instantly/list_emails.ts index 94e2b47e5ab..522f8ecfb6f 100644 --- a/apps/sim/tools/instantly/list_emails.ts +++ b/apps/sim/tools/instantly/list_emails.ts @@ -10,6 +10,7 @@ import { instantlyHeaders, instantlyUrl, mapEmail, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -68,14 +69,8 @@ export const listEmailsTool: ToolConfig { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const emails = getItems(data).map(mapEmail) return { diff --git a/apps/sim/tools/instantly/list_lead_lists.ts b/apps/sim/tools/instantly/list_lead_lists.ts index 0456fc3ff7d..02849521ae2 100644 --- a/apps/sim/tools/instantly/list_lead_lists.ts +++ b/apps/sim/tools/instantly/list_lead_lists.ts @@ -10,6 +10,7 @@ import { instantlyUrl, leadListsListOutputs, mapLeadList, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -60,7 +61,7 @@ export const listLeadListsTool: ToolConfig< headers: instantlyHeaders, }, transformResponse: async (response) => { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const leadLists = getItems(data).map(mapLeadList) return { diff --git a/apps/sim/tools/instantly/list_leads.ts b/apps/sim/tools/instantly/list_leads.ts index 1f81cd1ba85..cbfae6efc00 100644 --- a/apps/sim/tools/instantly/list_leads.ts +++ b/apps/sim/tools/instantly/list_leads.ts @@ -8,6 +8,7 @@ import { instantlyUrl, leadsListOutputs, mapLead, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -68,6 +69,19 @@ export const listLeadsTool: ToolConfig { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const leads = getItems(data).map(mapLead) return { diff --git a/apps/sim/tools/instantly/patch_campaign.ts b/apps/sim/tools/instantly/patch_campaign.ts index 26609b50597..8ec89425ce4 100644 --- a/apps/sim/tools/instantly/patch_campaign.ts +++ b/apps/sim/tools/instantly/patch_campaign.ts @@ -9,6 +9,7 @@ import { instantlyHeaders, instantlyUrl, mapCampaign, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -104,7 +105,7 @@ export const patchCampaignTool: ToolConfig< }, }, request: { - url: (params) => instantlyUrl(`/api/v2/campaigns/${params.campaignId}`), + url: (params) => instantlyUrl(`/api/v2/campaigns/${params.campaignId.trim()}`), method: 'PATCH', headers: instantlyHeaders, body: (params) => @@ -113,33 +114,18 @@ export const patchCampaignTool: ToolConfig< campaign_schedule: params.campaign_schedule, sequences: params.sequences, pl_value: params.pl_value, - is_evergreen: params.is_evergreen, email_gap: params.email_gap, - random_wait_max: params.random_wait_max, text_only: params.text_only, - first_email_text_only: params.first_email_text_only, email_list: params.email_list, daily_limit: params.daily_limit, stop_on_reply: params.stop_on_reply, - email_tag_list: params.email_tag_list, link_tracking: params.link_tracking, open_tracking: params.open_tracking, - stop_on_auto_reply: params.stop_on_auto_reply, daily_max_leads: params.daily_max_leads, - prioritize_new_leads: params.prioritize_new_leads, - match_lead_esp: params.match_lead_esp, - stop_for_company: params.stop_for_company, - insert_unsubscribe_header: params.insert_unsubscribe_header, - allow_risky_contacts: params.allow_risky_contacts, - disable_bounce_protect: params.disable_bounce_protect, - cc_list: params.cc_list, - bcc_list: params.bcc_list, - owned_by: params.owned_by, - ai_sdr_id: params.ai_sdr_id, }), }, transformResponse: async (response) => { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const campaign = mapCampaign(data) return { diff --git a/apps/sim/tools/instantly/reply_to_email.ts b/apps/sim/tools/instantly/reply_to_email.ts index fe201a18914..c980f2a450e 100644 --- a/apps/sim/tools/instantly/reply_to_email.ts +++ b/apps/sim/tools/instantly/reply_to_email.ts @@ -6,6 +6,7 @@ import { instantlyHeaders, instantlyUrl, mapEmail, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -68,7 +69,7 @@ export const replyToEmailTool: ToolConfig { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const email = mapEmail(data) return { diff --git a/apps/sim/tools/instantly/types.ts b/apps/sim/tools/instantly/types.ts index fe5575289c6..4c22923da63 100644 --- a/apps/sim/tools/instantly/types.ts +++ b/apps/sim/tools/instantly/types.ts @@ -163,29 +163,14 @@ export interface InstantlyCreateCampaignParams extends InstantlyBaseParams { campaign_schedule: Record sequences?: unknown[] pl_value?: number | null - is_evergreen?: boolean | null email_gap?: number | null - random_wait_max?: number | null text_only?: boolean | null - first_email_text_only?: boolean | null email_list?: string[] daily_limit?: number | null stop_on_reply?: boolean | null - email_tag_list?: string[] link_tracking?: boolean | null open_tracking?: boolean - stop_on_auto_reply?: boolean | null daily_max_leads?: number | null - prioritize_new_leads?: boolean | null - match_lead_esp?: boolean | null - stop_for_company?: boolean | null - insert_unsubscribe_header?: boolean | null - allow_risky_contacts?: boolean | null - disable_bounce_protect?: boolean | null - cc_list?: string[] - bcc_list?: string[] - owned_by?: string | null - ai_sdr_id?: string | null } export interface InstantlyPatchCampaignParams extends Partial { @@ -206,9 +191,7 @@ export interface InstantlyListEmailsParams extends InstantlyBaseParams { i_status?: number eaccount?: string lead?: string - lead_id?: string - is_unread?: number - thread_id?: string + is_unread?: boolean } export interface InstantlyReplyToEmailParams extends InstantlyBaseParams { diff --git a/apps/sim/tools/instantly/update_lead_interest_status.ts b/apps/sim/tools/instantly/update_lead_interest_status.ts index 1b7d9907c74..83e06078144 100644 --- a/apps/sim/tools/instantly/update_lead_interest_status.ts +++ b/apps/sim/tools/instantly/update_lead_interest_status.ts @@ -8,6 +8,7 @@ import { instantlyBaseParamFields, instantlyHeaders, instantlyUrl, + parseInstantlyResponse, } from '@/tools/instantly/utils' import type { ToolConfig } from '@/tools/types' @@ -29,9 +30,9 @@ export const updateLeadInterestStatusTool: ToolConfig< }, interest_value: { type: 'number', - required: true, + required: false, visibility: 'user-or-llm', - description: 'Interest status value. Use null in JSON/tool input to reset to Lead.', + description: 'Interest status value. Leave empty in the block or pass null to reset to Lead.', }, campaign_id: { type: 'string', @@ -62,18 +63,23 @@ export const updateLeadInterestStatusTool: ToolConfig< url: () => instantlyUrl('/api/v2/leads/update-interest-status'), method: 'POST', headers: instantlyHeaders, - body: (params) => - compactBody({ + body: (params) => { + if (params.interest_value === undefined) { + throw new Error('Interest Value is required for Instantly Update Lead Interest Status') + } + + return compactBody({ lead_email: params.lead_email, interest_value: params.interest_value, campaign_id: params.campaign_id, list_id: params.list_id, ai_interest_value: params.ai_interest_value, disable_auto_interest: params.disable_auto_interest, - }), + }) + }, }, transformResponse: async (response) => { - const data: unknown = await response.json() + const data = await parseInstantlyResponse(response) const result = asRecord(data) return { diff --git a/apps/sim/tools/instantly/utils.ts b/apps/sim/tools/instantly/utils.ts index 192f4c8fde8..885eab479a9 100644 --- a/apps/sim/tools/instantly/utils.ts +++ b/apps/sim/tools/instantly/utils.ts @@ -22,7 +22,7 @@ export const instantlyBaseParamFields = { } satisfies ToolConfig['params'] export const instantlyHeaders = (params: InstantlyBaseParams) => ({ - Authorization: `Bearer ${params.apiKey}`, + Authorization: `Bearer ${params.apiKey.trim()}`, 'Content-Type': 'application/json', }) @@ -43,6 +43,18 @@ export function compactBody(values: Record): Record { + const data = await parseJsonResponse(response) + + if (!response.ok) { + throw new Error( + extractInstantlyError(data, `Instantly API request failed (${response.status})`) + ) + } + + return data +} + export function asRecord(value: unknown): JsonRecord { return isRecord(value) ? value : {} } @@ -326,6 +338,21 @@ function isRecord(value: unknown): value is JsonRecord { return typeof value === 'object' && value !== null && !Array.isArray(value) } +async function parseJsonResponse(response: Response): Promise { + try { + return await response.json() + } catch { + return null + } +} + +function extractInstantlyError(value: unknown, fallback: string): string { + const data = asRecord(value) + if (typeof data.message === 'string') return data.message + if (typeof data.error === 'string') return data.error + return fallback +} + function asString(value: unknown): string | null { return typeof value === 'string' ? value : null } diff --git a/apps/sim/triggers/instantly/utils.ts b/apps/sim/triggers/instantly/utils.ts index f666043110a..4581a7ee57c 100644 --- a/apps/sim/triggers/instantly/utils.ts +++ b/apps/sim/triggers/instantly/utils.ts @@ -75,7 +75,7 @@ export function instantlySetupInstructions(eventType: string): string { export function buildInstantlyExtraFields(triggerId: string): SubBlockConfig[] { return [ { - id: 'apiKey', + id: 'triggerApiKey', title: 'API Key', type: 'short-input', placeholder: 'Enter your Instantly API key', From 008e018b1c50f5274d92ad1d755daa35f1d91f17 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 27 May 2026 16:20:06 -0700 Subject: [PATCH 4/5] cleanup code --- apps/sim/blocks/blocks/instantly.ts | 136 ++++++++++++++++++++-------- 1 file changed, 97 insertions(+), 39 deletions(-) diff --git a/apps/sim/blocks/blocks/instantly.ts b/apps/sim/blocks/blocks/instantly.ts index 695b3782a87..f514dc1a18a 100644 --- a/apps/sim/blocks/blocks/instantly.ts +++ b/apps/sim/blocks/blocks/instantly.ts @@ -861,24 +861,9 @@ export const InstantlyBlock: BlockConfig = { config: { tool: (params) => `instantly_${params.operation}`, params: (params) => ({ - campaign: - params.operation === 'list_leads' - ? optionalIdParam(params.campaignId) - : params.leadDestination === 'campaign' - ? optionalIdParam(params.leadDestinationId) - : undefined, - list_id: - params.operation === 'delete_leads' && params.deleteSource === 'list' - ? optionalIdParam(params.deleteSourceId) - : params.leadDestination === 'list' - ? optionalIdParam(params.leadDestinationId) - : optionalIdParam(params.listId), - campaign_id: - params.operation === 'delete_leads' - ? params.deleteSource === 'campaign' - ? optionalIdParam(params.deleteSourceId) - : undefined - : optionalIdParam(params.campaignId), + campaign: mapCampaignParam(params), + list_id: mapListIdParam(params), + campaign_id: mapCampaignIdParam(params), leadId: params.leadId, email: emptyToUndefined(params.email), first_name: emptyToUndefined(params.firstName), @@ -899,10 +884,7 @@ export const InstantlyBlock: BlockConfig = { verify_leads_for_lead_finder: toBooleanParam(params.verifyLeadsForLeadFinder), verify_leads_on_import: toBooleanParam(params.verifyLeadsOnImport), filter: emptyToUndefined(params.leadFilter), - ids: - params.operation === 'delete_leads' - ? parseStringList(params.deleteLeadIds) - : parseStringList(params.leadIds), + ids: mapIdsParam(params), excluded_ids: parseStringList(params.excludedLeadIds), contacts: parseStringList(params.contacts), organization_user_ids: parseStringList(params.organizationUserIds), @@ -913,15 +895,9 @@ export const InstantlyBlock: BlockConfig = { esg_code: emptyToUndefined(params.esgCode), in_campaign: toBooleanParam(params.inCampaign), in_list: toBooleanParam(params.inList), - status: - params.operation === 'delete_leads' - ? toNumberParam(params.deleteStatus) - : toNumberParam(params.campaignStatus), - limit: - params.operation === 'delete_leads' - ? toNumberParam(params.deleteLimit) - : toNumberParam(params.limit), - starting_after: emptyToUndefined(params.startingAfter), + status: mapStatusParam(params), + limit: mapLimitParam(params), + starting_after: mapStartingAfterParam(params), lead_email: emptyToUndefined(params.leadEmail), interest_value: params.operation === 'update_lead_interest_status' @@ -929,10 +905,7 @@ export const InstantlyBlock: BlockConfig = { : undefined, ai_interest_value: toNumberParam(params.aiInterestValue), disable_auto_interest: toBooleanParam(params.disableAutoInterest), - name: - params.operation === 'create_lead_list' - ? emptyToUndefined(params.leadListName) - : emptyToUndefined(params.campaignName), + name: mapNameParam(params), campaign_schedule: parseJsonObject(params.campaignSchedule), sequences: parseJsonArray(params.sequences), email_list: parseStringList(params.emailList), @@ -946,10 +919,7 @@ export const InstantlyBlock: BlockConfig = { text_only: toBooleanParam(params.textOnly), tag_ids: emptyToUndefined(params.tagIds), ai_sales_agent_id: optionalIdParam(params.aiSalesAgentId), - search: - params.operation === 'list_emails' - ? emptyToUndefined(params.emailSearch) - : emptyToUndefined(params.search), + search: mapSearchParam(params), eaccount: emptyToUndefined(params.emailAccount), i_status: toNumberParam(params.emailStatus), lead: emptyToUndefined(params.emailLead), @@ -1190,6 +1160,94 @@ function emptyToUndefined(value: unknown): unknown { return trimmed === '' || trimmed === '-' ? undefined : trimmed } +function mapCampaignParam(params: Record): string | undefined { + if (params.operation === 'list_leads') return optionalIdParam(params.campaignId) + if (params.operation !== 'create_lead' || params.leadDestination !== 'campaign') return undefined + return optionalIdParam(params.leadDestinationId) +} + +function mapListIdParam(params: Record): string | undefined { + switch (params.operation) { + case 'delete_leads': + return params.deleteSource === 'list' ? optionalIdParam(params.deleteSourceId) : undefined + case 'create_lead': + return params.leadDestination === 'list' + ? optionalIdParam(params.leadDestinationId) + : undefined + case 'list_leads': + case 'update_lead_interest_status': + case 'list_emails': + return optionalIdParam(params.listId) + default: + return undefined + } +} + +function mapCampaignIdParam(params: Record): string | undefined { + if (params.operation === 'delete_leads') { + return params.deleteSource === 'campaign' ? optionalIdParam(params.deleteSourceId) : undefined + } + + if (params.operation === 'update_lead_interest_status' || params.operation === 'list_emails') { + return optionalIdParam(params.campaignId) + } + + return undefined +} + +function mapIdsParam(params: Record): string[] | undefined { + if (params.operation === 'delete_leads') return parseStringList(params.deleteLeadIds) + if (params.operation === 'list_leads') return parseStringList(params.leadIds) + return undefined +} + +function mapStatusParam(params: Record): number | undefined { + if (params.operation === 'delete_leads') return toNumberParam(params.deleteStatus) + if (params.operation === 'list_campaigns') return toNumberParam(params.campaignStatus) + return undefined +} + +function mapLimitParam(params: Record): number | undefined { + if (params.operation === 'delete_leads') return toNumberParam(params.deleteLimit) + if (isPaginatedOperation(params.operation)) return toNumberParam(params.limit) + return undefined +} + +function mapStartingAfterParam(params: Record): unknown { + return isPaginatedOperation(params.operation) ? emptyToUndefined(params.startingAfter) : undefined +} + +function mapNameParam(params: Record): unknown { + switch (params.operation) { + case 'create_lead_list': + return emptyToUndefined(params.leadListName) + case 'create_campaign': + case 'patch_campaign': + return emptyToUndefined(params.campaignName) + default: + return undefined + } +} + +function mapSearchParam(params: Record): unknown { + if (params.operation === 'list_emails') return emptyToUndefined(params.emailSearch) + if (isSearchOperation(params.operation)) return emptyToUndefined(params.search) + return undefined +} + +function isPaginatedOperation(value: unknown): boolean { + return ( + value === 'list_leads' || + value === 'list_campaigns' || + value === 'list_emails' || + value === 'list_lead_lists' + ) +} + +function isSearchOperation(value: unknown): boolean { + return value === 'list_leads' || value === 'list_campaigns' || value === 'list_lead_lists' +} + function optionalIdParam(value: unknown): string | undefined { if (typeof value !== 'string') return undefined const trimmed = value.trim() From 4c9d71708d46632741db3796bf881872df1c4977 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 27 May 2026 16:28:50 -0700 Subject: [PATCH 5/5] search param missplaced --- apps/sim/blocks/blocks/instantly.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/sim/blocks/blocks/instantly.ts b/apps/sim/blocks/blocks/instantly.ts index f514dc1a18a..76c8d0872c6 100644 --- a/apps/sim/blocks/blocks/instantly.ts +++ b/apps/sim/blocks/blocks/instantly.ts @@ -300,12 +300,7 @@ export const InstantlyBlock: BlockConfig = { placeholder: 'Search term', condition: { field: 'operation', - value: [ - ...LEAD_LIST_OPERATIONS, - ...CAMPAIGN_LIST_OPERATIONS, - ...EMAIL_LIST_OPERATIONS, - ...LEAD_LIST_LIST_OPERATIONS, - ], + value: [...LEAD_LIST_OPERATIONS, ...CAMPAIGN_LIST_OPERATIONS, ...LEAD_LIST_LIST_OPERATIONS], }, }, {