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..811fefde4ca --- /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..0832536e1de 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": "#FFFFFF", + "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..76c8d0872c6 --- /dev/null +++ b/apps/sim/blocks/blocks/instantly.ts @@ -0,0 +1,1255 @@ +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: '#FFFFFF', + 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, + paramVisibility: 'user-only', + }, + { + 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], + and: { field: 'leadDestination', value: 'campaign' }, + }, + 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 - no explanations, no extra text.', + 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: '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', + type: 'short-input', + placeholder: 'Search term', + condition: { + field: 'operation', + value: [...LEAD_LIST_OPERATIONS, ...CAMPAIGN_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: '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', + 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: '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', + 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 or null', + 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: 'aiInterestValue', + title: 'AI Interest Value', + type: 'short-input', + placeholder: '1', + 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 - no explanations, no extra text.', + 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 - no explanations, no extra text.', + 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: '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', + type: 'short-input', + placeholder: 'id1,id2', + 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', + 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: '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', + 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: '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', + 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: mapCampaignParam(params), + list_id: mapListIdParam(params), + campaign_id: mapCampaignIdParam(params), + leadId: params.leadId, + 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: mapIdsParam(params), + 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: mapStatusParam(params), + limit: mapLimitParam(params), + starting_after: mapStartingAfterParam(params), + 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: mapNameParam(params), + 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), + 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: mapSearchParam(params), + 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: 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: optionalIdParam(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' }, + 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 (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' }, + 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') + .map((item) => item.trim()) + .filter((item) => item !== '' && item !== '-') + return strings.length > 0 ? strings : undefined + } + + if (typeof value !== 'string') return undefined + const trimmed = value.trim() + if (trimmed === '' || trimmed === '-') return undefined + + const strings = trimmed + .split(/[\s,]+/) + .map((item) => item.trim()) + .filter((item) => item !== '' && item !== '-') + + 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 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 + if (value === 'true') return true + if (value === 'false') return false + return undefined +} + +function emptyToUndefined(value: unknown): unknown { + if (typeof value !== 'string') return value + const trimmed = value.trim() + 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() + if (trimmed === '' || trimmed === '-') return undefined + return trimmed +} + +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..79e8191546f 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' @@ -364,6 +365,7 @@ export const registry: Record = { incidentio: IncidentioBlock, infisical: InfisicalBlock, input_trigger: InputTriggerBlock, + instantly: InstantlyBlock, intercom: IntercomBlock, intercom_v2: IntercomV2Block, jina: JinaBlock, 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..17bd554e734 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/instantly.ts @@ -0,0 +1,269 @@ +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) { + 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`) + 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.triggerApiKey as string | undefined + const triggerId = providerConfig.triggerId as string | undefined + const campaignId = optionalId(providerConfig.triggerCampaignId) + + 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) { + requestBody.campaign = campaignId + } + + 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.trim()}`, + '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.triggerApiKey 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.trim())}`), + { + method: 'DELETE', + headers: { + Authorization: `Bearer ${apiKey.trim()}`, + }, + } + ) + + 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 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/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..b9755b082ea --- /dev/null +++ b/apps/sim/tools/instantly/activate_campaign.ts @@ -0,0 +1,52 @@ +import type { + InstantlyActivateCampaignParams, + InstantlyCampaignResponse, +} from '@/tools/instantly/types' +import { + campaignOutputs, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + mapCampaign, + parseInstantlyResponse, +} 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.trim()}/activate`), + method: 'POST', + headers: instantlyHeaders, + }, + transformResponse: async (response) => { + const data = await parseInstantlyResponse(response) + const campaign = mapCampaign(data) + + return { + success: true, + output: { + campaign, + id: campaign.id, + name: campaign.name, + status: campaign.status, + }, + } + }, + 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..d900d912c42 --- /dev/null +++ b/apps/sim/tools/instantly/create_campaign.ts @@ -0,0 +1,136 @@ +import type { + InstantlyCampaignResponse, + InstantlyCreateCampaignParams, +} from '@/tools/instantly/types' +import { + campaignOutputs, + compactBody, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + mapCampaign, + parseInstantlyResponse, +} 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, + email_gap: params.email_gap, + text_only: params.text_only, + email_list: params.email_list, + daily_limit: params.daily_limit, + stop_on_reply: params.stop_on_reply, + link_tracking: params.link_tracking, + open_tracking: params.open_tracking, + daily_max_leads: params.daily_max_leads, + }), + }, + transformResponse: async (response) => { + const data = await parseInstantlyResponse(response) + const campaign = mapCampaign(data) + + return { + success: true, + output: { + campaign, + id: campaign.id, + name: campaign.name, + status: campaign.status, + }, + } + }, + 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..21ed22974f7 --- /dev/null +++ b/apps/sim/tools/instantly/create_lead.ts @@ -0,0 +1,187 @@ +import type { InstantlyCreateLeadParams, InstantlyLeadResponse } from '@/tools/instantly/types' +import { + compactBody, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + leadOutputs, + mapLead, + parseInstantlyResponse, +} 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_for_lead_finder: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to verify leads imported from Lead Finder', + }, + 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 = await parseInstantlyResponse(response) + const lead = mapLead(data) + + return { + success: true, + output: { + lead, + id: lead.id, + email_address: lead.email, + first_name: lead.first_name, + last_name: lead.last_name, + campaign: lead.campaign, + status: lead.status, + }, + } + }, + outputs: leadOutputs, +} 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..288b61ca50a --- /dev/null +++ b/apps/sim/tools/instantly/create_lead_list.ts @@ -0,0 +1,70 @@ +import type { + InstantlyCreateLeadListParams, + InstantlyLeadListResponse, +} from '@/tools/instantly/types' +import { + compactBody, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + leadListOutputs, + mapLeadList, + parseInstantlyResponse, +} 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 = await parseInstantlyResponse(response) + 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..8e313a2df8e --- /dev/null +++ b/apps/sim/tools/instantly/delete_leads.ts @@ -0,0 +1,82 @@ +import type { + InstantlyDeleteLeadsParams, + InstantlyDeleteLeadsResponse, +} from '@/tools/instantly/types' +import { + asRecord, + compactBody, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + parseInstantlyResponse, +} 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 = await parseInstantlyResponse(response) + 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..1f7239d68ff --- /dev/null +++ b/apps/sim/tools/instantly/get_lead.ts @@ -0,0 +1,49 @@ +import type { InstantlyGetLeadParams, InstantlyLeadResponse } from '@/tools/instantly/types' +import { + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + leadOutputs, + mapLead, + parseInstantlyResponse, +} 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.trim()}`), + method: 'GET', + headers: instantlyHeaders, + }, + transformResponse: async (response) => { + const data = await parseInstantlyResponse(response) + const lead = mapLead(data) + + return { + success: true, + output: { + lead, + id: lead.id, + email_address: lead.email, + first_name: lead.first_name, + last_name: lead.last_name, + campaign: lead.campaign, + status: lead.status, + }, + } + }, + outputs: leadOutputs, +} 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..fa51ca23b96 --- /dev/null +++ b/apps/sim/tools/instantly/list_campaigns.ts @@ -0,0 +1,91 @@ +import type { + InstantlyListCampaignsParams, + InstantlyListCampaignsResponse, +} from '@/tools/instantly/types' +import { + campaignsListOutputs, + getItems, + getNextStartingAfter, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + mapCampaign, + parseInstantlyResponse, +} 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 = await parseInstantlyResponse(response) + 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..522f8ecfb6f --- /dev/null +++ b/apps/sim/tools/instantly/list_emails.ts @@ -0,0 +1,109 @@ +import type { + InstantlyListEmailsParams, + InstantlyListEmailsResponse, +} from '@/tools/instantly/types' +import { + emailsListOutputs, + getItems, + getNextStartingAfter, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + mapEmail, + parseInstantlyResponse, +} 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', + }, + is_unread: { + type: 'boolean', + 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.search, + campaign_id: params.campaign_id, + list_id: params.list_id, + i_status: params.i_status, + eaccount: params.eaccount, + lead: params.lead, + is_unread: params.is_unread, + }), + method: 'GET', + headers: instantlyHeaders, + }, + transformResponse: async (response) => { + const data = await parseInstantlyResponse(response) + 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..02849521ae2 --- /dev/null +++ b/apps/sim/tools/instantly/list_lead_lists.ts @@ -0,0 +1,77 @@ +import type { + InstantlyListLeadListsParams, + InstantlyListLeadListsResponse, +} from '@/tools/instantly/types' +import { + getItems, + getNextStartingAfter, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + leadListsListOutputs, + mapLeadList, + parseInstantlyResponse, +} 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 = await parseInstantlyResponse(response) + 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..cbfae6efc00 --- /dev/null +++ b/apps/sim/tools/instantly/list_leads.ts @@ -0,0 +1,168 @@ +import type { InstantlyListLeadsParams, InstantlyListLeadsResponse } from '@/tools/instantly/types' +import { + compactBody, + getItems, + getNextStartingAfter, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + leadsListOutputs, + mapLead, + parseInstantlyResponse, +} 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' }, + }, + organization_user_ids: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Organization user IDs to filter leads', + items: { type: 'string', description: 'Organization user ID' }, + }, + smart_view_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Smart view ID to filter leads', + }, + 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', + }, + is_website_visitor: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the lead is a website visitor', + }, + 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 = await parseInstantlyResponse(response) + 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..8ec89425ce4 --- /dev/null +++ b/apps/sim/tools/instantly/patch_campaign.ts @@ -0,0 +1,142 @@ +import type { + InstantlyCampaignResponse, + InstantlyPatchCampaignParams, +} from '@/tools/instantly/types' +import { + campaignOutputs, + compactBody, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + mapCampaign, + parseInstantlyResponse, +} 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.trim()}`), + method: 'PATCH', + headers: instantlyHeaders, + body: (params) => + compactBody({ + name: params.name, + campaign_schedule: params.campaign_schedule, + sequences: params.sequences, + pl_value: params.pl_value, + email_gap: params.email_gap, + text_only: params.text_only, + email_list: params.email_list, + daily_limit: params.daily_limit, + stop_on_reply: params.stop_on_reply, + link_tracking: params.link_tracking, + open_tracking: params.open_tracking, + daily_max_leads: params.daily_max_leads, + }), + }, + transformResponse: async (response) => { + const data = await parseInstantlyResponse(response) + const campaign = mapCampaign(data) + + return { + success: true, + output: { + campaign, + id: campaign.id, + name: campaign.name, + status: campaign.status, + }, + } + }, + 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..c980f2a450e --- /dev/null +++ b/apps/sim/tools/instantly/reply_to_email.ts @@ -0,0 +1,86 @@ +import type { InstantlyEmailResponse, InstantlyReplyToEmailParams } from '@/tools/instantly/types' +import { + compactBody, + emailOutputs, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + mapEmail, + parseInstantlyResponse, +} 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 = await parseInstantlyResponse(response) + 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..4c22923da63 --- /dev/null +++ b/apps/sim/tools/instantly/types.ts @@ -0,0 +1,314 @@ +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 + email_gap?: number | null + text_only?: boolean | null + email_list?: string[] + daily_limit?: number | null + stop_on_reply?: boolean | null + link_tracking?: boolean | null + open_tracking?: boolean + daily_max_leads?: number | 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 + is_unread?: boolean +} + +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..83e06078144 --- /dev/null +++ b/apps/sim/tools/instantly/update_lead_interest_status.ts @@ -0,0 +1,99 @@ +import type { + InstantlyUpdateLeadInterestStatusParams, + InstantlyUpdateLeadInterestStatusResponse, +} from '@/tools/instantly/types' +import { + asRecord, + compactBody, + instantlyBaseParamFields, + instantlyHeaders, + instantlyUrl, + parseInstantlyResponse, +} 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: false, + visibility: 'user-or-llm', + description: 'Interest status value. Leave empty in the block or pass null 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) => { + 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 = await parseInstantlyResponse(response) + 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..885eab479a9 --- /dev/null +++ b/apps/sim/tools/instantly/utils.ts @@ -0,0 +1,366 @@ +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.trim()}`, + '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 async function parseInstantlyResponse(response: Response): Promise { + 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 : {} +} + +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) +} + +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 +} + +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..4581a7ee57c --- /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: 'triggerApiKey', + 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,