Workflow Node Vocabulary
Complete vocabulary of workflow node types for building automation graphs. Use campaignstack_get_workflow_node_types to fetch this data at runtime, or pass a graph object to campaignstack_create_workflow / campaignstack_update_workflow.
Graph Shape
Section titled “Graph Shape”A workflow graph is { nodes, edges }. nodes is an ordered array of WorkflowNode objects. edges connect nodes by their 0-based array indices. The source:entry anchor node is always auto-created - do not include it in nodes. Use sourceConfig to configure lead sources (providers, dailyCap, exclusions).
Node fields
Section titled “Node fields”\{ nodeType: string, label: string, config: object \} - position is auto-computed and can be omitted
Edge fields
Section titled “Edge fields”\{ sourceIndex: number, targetIndex: number, label?: string \} - label is required for branching nodes (e.g. 'true'/'false' for flow:if, 'accepted'/'timeout' for linkedin:reactive with connection_accepted)
Minimal Example Graph
Section titled “Minimal Example Graph”{ "nodes": [ { "nodeType": "linkedin:profile_view", "label": "View Profile", "config": {} }, { "nodeType": "flow:wait", "label": "Wait 3 Days", "config": { "waitType": "duration", "durationMs": 259200000 } }, { "nodeType": "linkedin:connection_request", "label": "Send Connection Request", "config": { "noteMode": "none" } } ], "edges": [ { "sourceIndex": 0, "targetIndex": 1 }, { "sourceIndex": 1, "targetIndex": 2 } ]}Validation Rules
Section titled “Validation Rules”Rules enforced by campaignstack_create_workflow and campaignstack_update_workflow:
- At least one node — the graph must contain at least one node.
- No source:entry in nodes — the source:entry anchor node is always auto-created from
sourceConfig. Do not include it in thenodesarray. - No signal:intake or signal:message in nodes — these anchor nodes are preserved automatically in signal-agent workflows. Connect them to your nodes via
anchorEdgesinstead. - Valid node types — every
nodeTypemust exist in the vocabulary below. - Label required — every node must have a non-empty string
label. - Config required — every node must have a
configobject (use\{\}for nodes with no config). - Valid edge indices — edge
sourceIndexandtargetIndexmust be valid 0-based indices into thenodesarray. No self-loops. - flow:if edges — all outgoing edges from a
flow:ifnode must havelabel: "true"orlabel: "false". - Multi-exit edges — outgoing edges from branching nodes (e.g.
review:content,signal:message,linkedin:reactive) must use the node’s declared exit labels (see each node’s exits field below). - Single-exit nodes — all other nodes may have at most 1 outgoing edge.
- Auto-fixes —
flow:waitwithoutdurationMsdefaults to 86 400 000 ms (1 day);linkedin:reactivewith an invalidtimeoutDurationdefaults to 168 h;linkedin:connection_requestwithnoteMode: "ai"is silently downgraded to"none". - anchorEdges — for signal-agent graphs,
anchorEdgesconnect the preserved anchor node to your graph. Each must have a validtargetIndexand alabelin["dm", "cr", "reply", "liked", "no_action"].
Node Types (37 types)
Section titled “Node Types (37 types)”linkedin:profile_view
Section titled “linkedin:profile_view”Label: Profile View | Category: linkedin | Credit cost: 10 cr
Views the lead’s LinkedIn profile to trigger a notification
Config:
{}Example:
{ "nodeType": "linkedin:profile_view", "label": "View Profile", "config": {}}linkedin:post_like
Section titled “linkedin:post_like”Label: Post Like | Category: linkedin | Credit cost: 10 cr
Finds and likes the lead’s most recent LinkedIn post
Config:
{}Example:
{ "nodeType": "linkedin:post_like", "label": "Like Post", "config": {}}linkedin:post_comment
Section titled “linkedin:post_comment”Label: Post Comment | Category: linkedin | Credit cost: 10 cr
Reads the lead’s latest post and leaves a relevant comment
Config:
{ commentText?: string }Example:
{ "nodeType": "linkedin:post_comment", "label": "Comment on Post", "config": {}}linkedin:reply_comment
Section titled “linkedin:reply_comment”Label: Reply to Comment | Category: linkedin | Credit cost: 10 cr
Replies to a specific comment on a LinkedIn post - navigates to the post, finds the comment by author name and text, and posts the approved reply
Config:
{}Example:
{ "nodeType": "linkedin:reply_comment", "label": "Reply to Comment", "config": {}}linkedin:connection_request
Section titled “linkedin:connection_request”Label: Connection Request | Category: linkedin | Credit cost: 10 cr
Sends a connection request. Choose between AI-personalized, template, or no note
Config:
{ noteMode: "none"|"static", noteTemplate?: string } - for a personalized note, build ai:craft_note → review:content → linkedin:connection_request { noteMode: "static" } (the note flows from context). The connection_request node does not self-generate AI notes.Example:
{ "nodeType": "linkedin:connection_request", "label": "Send Connection Request", "config": { "noteMode": "none" }}linkedin:withdraw_invitation
Section titled “linkedin:withdraw_invitation”Label: Withdraw Invitation | Category: linkedin | Credit cost: 1 cr
Withdraws a previously sent LinkedIn connection invitation so the workflow can fall through to another channel
Config:
{} - withdraws the sent connection invitation for this lead on the entry's assigned account; needs only the lead's LinkedIn identity, not a fresh profile scrape. Exits: withdrawn (invitation retracted), already_accepted (the lead is already 1st-degree connected, or the invitation was no longer in the sent list - already accepted, withdrawn elsewhere, or expired).Exits: withdrawn, already_accepted
Example:
{ "nodeType": "linkedin:withdraw_invitation", "label": "Withdraw Invitation", "config": {}}linkedin:send_message
Section titled “linkedin:send_message”Label: Send Message | Category: linkedin | Credit cost: 10 cr
Sends a direct message to the lead. Supports AI-generated or template text
Config:
{ messageTemplate?: string, useAi?: boolean, attachments?: Array<{storageId, filename, mimeType, size}> } - send literal messageTemplate or approved upstream ai:craft_message content. Do not generate only { useAi: true }; for automatic personalized DMs use linkedin:research_and_message.Example:
{ "nodeType": "linkedin:send_message", "label": "Send Message", "config": { "messageTemplate": "Hi {{lead:firstName}}, I noticed your work at {{lead:company}} and wanted to connect." }}linkedin:send_followup
Section titled “linkedin:send_followup”Label: Send Follow-up | Category: linkedin | Credit cost: 10 cr
Sends a follow-up message in an existing conversation thread
Config:
{ messageTemplate?: string, useAi?: boolean } - send follow-up in existing conversation thread. Requires a prior send_message or research_and_message node upstream so a conversation exists. useAi=true calls craft_followup for personalized text.Example:
{ "nodeType": "linkedin:send_followup", "label": "Send Follow-up", "config": { "useAi": true }}linkedin:inbox_read
Section titled “linkedin:inbox_read”Label: Inbox Read | Category: linkedin | Credit cost: 0 cr
Reads recent messages from a conversation with the lead
Config:
{ pollIntervalMs?: number (in milliseconds, e.g. 60000 = 1 minute) }Example:
{ "nodeType": "linkedin:inbox_read", "label": "Read Inbox", "config": {}}linkedin:research_and_message
Section titled “linkedin:research_and_message”Label: Research & Message | Category: linkedin | Credit cost: 15 cr
Researches the lead’s profile and posts, then sends a personalized message based on findings
Config:
{ messageInstructions?: string, maxPosts?: number }Example:
{ "nodeType": "linkedin:research_and_message", "label": "Research & Message", "config": { "messageInstructions": "Focus on their recent posts and company news." }}signal:intake
Section titled “signal:intake”Label: Signal Intake | Category: signal | Credit cost: 15 cr
Decides the best action for a signal (DM, CR, reply, like, or skip) and crafts content - downstream nodes handle actual sending
Note: This node type is not available in the visual workflow builder. It is used internally by signal-agent workflows.
Config:
{ crNoteMode?: "none"|"static"|"with_note", crNoteTemplate?: string, reviewMode?: "on"|"off", publicReplyEnabled?: boolean, allowConnectionRequest?: boolean } - signal-agent only. Exits: dm, cr, liked, no_action, plus reply when publicReplyEnabled is true. allowConnectionRequest: false downgrades would-be CRs to liked.Example:
{ "nodeType": "signal:intake", "label": "Signal Intake", "config": { "crNoteMode": "none", "reviewMode": "off" }}signal:message
Section titled “signal:message”Label: Message Signal Intake | Category: signal | Credit cost: 10 cr
Receives an inbound LinkedIn message, decides the best response, and crafts a reply - downstream nodes handle review and sending
Config:
{ reviewMode: "on"|"off", handleInMail?: boolean } - message signal agent only. Exits: respond, meeting_scheduled (inbound message carried a calendar link and a meeting was booked), escalate, ignore (non-actionable message; the default when no exit applies - leads stop here unless the exit is connected).Exits: respond, meeting_scheduled, escalate, ignore
Example:
{ "nodeType": "signal:message", "label": "Message Signal Intake", "config": { "reviewMode": "off" }}group:intake
Section titled “group:intake”Label: Group Intake | Category: group | Credit cost: 0 cr
Entry point for group watch workflows - routes ICP-matching group members to the outreach chain. Optionally parks leads for human review before outreach.
Note: This node type is not available in the visual workflow builder. It is used internally by signal-agent workflows.
Config:
{ reviewMode?: "on"|"off" }Example:
{ "nodeType": "group:intake", "label": "Group Intake", "config": { "reviewMode": "off" }}connection:intake
Section titled “connection:intake”Label: Connection Intake | Category: connection | Credit cost: 0 cr
Entry point for connection-watch workflows - receives newly accepted connections and routes them to the welcome chain (craft → review → send)
Note: This node type is not available in the visual workflow builder. It is used internally by signal-agent workflows.
Config:
{ reviewMode?: "on"|"off" } - connection-watch signal agent only. The review gate lives on the crafted message, not the intake.Example:
{ "nodeType": "connection:intake", "label": "Connection Intake", "config": { "reviewMode": "off" }}linkedin:contact_info
Section titled “linkedin:contact_info”Label: Extract Contact Info | Category: linkedin | Credit cost: 10 cr
Extracts email, phone, and website from LinkedIn Contact Info overlay. 1st-degree connections only.
Config:
{}Example:
{ "nodeType": "linkedin:contact_info", "label": "Extract Contact Info", "config": {}}linkedin:reactive
Section titled “linkedin:reactive”Label: Reactive Node | Category: linkedin | Credit cost: 0 cr
Parks the lead and waits for a LinkedIn event (e.g. message reply, connection acceptance) before continuing
Config:
{ eventType: "message_received"|"connection_accepted", timeoutDuration: number (in hours, e.g. 168 = 7 days, 336 = 14 days; valid range: 1-720), responseMode?: "auto_respond"|"draft_and_review", autoBooking?: boolean } - reply and escalation are always enabled. autoBooking: when the lead sends a scheduling link, book a mutual slot via the account’s connected calendar (auto_respond only; needs a calendar-connected account). Escalation parks the lead (creates a pending action, no exit). connection_accepted exits: accepted, timeout. message_received + draft_and_review exits: draft_ready, timeout. message_received + auto_respond exits: replied, timeout (+ meeting_scheduled when autoBooking enabled).Example:
{ "nodeType": "linkedin:reactive", "label": "Wait for Reply", "config": { "eventType": "message_received", "timeoutDuration": 168 }}linkedin:reactive_respond
Section titled “linkedin:reactive_respond”Label: Reactive Responder | Category: linkedin | Credit cost: 10 cr
Browser-based agent that reads a new LinkedIn message and responds using configured tools. Routes to sent_message, needs_human, or no_action based on which tool was used.
Config:
{ instructions?: string, enabledTools?: string[] } - runner responder used after a reactive message event.Example:
{ "nodeType": "linkedin:reactive_respond", "label": "Respond to Reply", "config": { "instructions": "Be helpful and professional." }}google:email_send
Section titled “google:email_send”Label: Send Email | Category: google | Credit cost: 2 cr
Sends an email via Gmail. Static mode resolves {{lead:*}} template variables. Pair with ai:craft_email for AI-personalized copy.
Config:
{ mode?: "static"|"crafted"|"auto_generate", googleAccountId?: string, subject?: string, bodyTemplate?: string, htmlBodyTemplate?: string, attachments?: Array<{storageId, filename, mimeType, size}>, autoGenerateInstructions?: string, tone?: string (auto_generate mode: writing tone, e.g. "professional"), includeHtml?: boolean (auto_generate mode: also produce an HTML body), blockOnFailure?: boolean } - static mode resolves {{lead:firstName}}, {{lead:company}}, etc. in subject/body. crafted mode reads upstream craftedSubject/craftedBody from an ai:craft_email node. auto_generate mode generates subject+body via AI in one step and costs 17 credits total (2 send + 15 craft, the same craft price as ai:craft_email); use autoGenerateInstructions to guide the AI. Fails if lead has no email address. Best practices: use static mode for low-context leads (name + email only, no profile data). Keep body under 80 words, subject 2-4 words lowercase, zero selling language (no "free", "guaranteed", "exclusive"). For multi-email sequences, place a flow:wait { waitType: "email_event", emailEventType: "click", timeoutHours: 96 } after each send so leads who click exit the sequence and only non-engagers receive the next email. Each email in a sequence should target a separate pain point rather than repeating the same pitch. blockOnFailure defaults to true on the backend.Example:
{ "nodeType": "google:email_send", "label": "Send Email", "config": { "mode": "crafted" }}ai:autonomous
Section titled “ai:autonomous”Label: Autonomous Agent | Category: ai | Credit cost: 10 cr
Executes custom instructions using any combination of LinkedIn actions in a single session
Config:
{ instructions: string }Example:
{ "nodeType": "ai:autonomous", "label": "Autonomous Agent", "config": { "instructions": "Research the lead's profile and send a personalized message referencing their recent activity." }}ai:craft_note
Section titled “ai:craft_note”Label: Craft Connection Note | Category: ai | Credit cost: 5 cr
Generates a personalized connection request note (max 300 chars)
Config:
{ instructions?: string }Example:
{ "nodeType": "ai:craft_note", "label": "Craft Connection Note", "config": {}}ai:craft_message
Section titled “ai:craft_message”Label: Craft Message | Category: ai | Credit cost: 5 cr
Generates a personalized outreach message based on lead profile and campaign goals
Config:
{ instructions?: string, messageTemplate?: string }Example:
{ "nodeType": "ai:craft_message", "label": "Craft Message", "config": {}}ai:craft_comment
Section titled “ai:craft_comment”Label: Craft Comment | Category: ai | Credit cost: 5 cr
Generates a relevant comment for a lead’s latest post
Config:
{ instructions?: string }Example:
{ "nodeType": "ai:craft_comment", "label": "Craft Comment", "config": {}}ai:reply_conversation
Section titled “ai:reply_conversation”Label: Reply to Conversation | Category: ai | Credit cost: 5 cr
Generates a contextual follow-up reply based on conversation history
Config:
{ instructions?: string }Example:
{ "nodeType": "ai:reply_conversation", "label": "Reply to Conversation", "config": {}}ai:craft_email
Section titled “ai:craft_email”Label: Craft Email | Category: ai | Credit cost: 15 cr
Generates a personalized email (subject + plain text body, with optional HTML) based on lead profile, campaign goals, and workspace voice
Config:
{ instructions?: string, tone?: string, includeSignature?: boolean } - generates craftedSubject + craftedBody (plain text) + craftedHtmlBody (HTML, only when instructions or voice mention HTML/formatting). Pair with a google:email_send node to deliver the email. Use crafted mode when leads have rich profile data (headline, company, experience) worth personalizing against. For leads with only name + email (no profile context), static mode on google:email_send is better because AI will produce nearly identical generic emails and waste credits.Example:
{ "nodeType": "ai:craft_email", "label": "Craft Email", "config": { "tone": "professional", "includeSignature": true }}review:content
Section titled “review:content”Label: Review Content | Category: review | Credit cost: 0 cr
Pauses workflow for human review of AI-generated content. Supports approve, edit, retry, and reject actions.
Config:
{ contentNodeId?: string } - optional reference to upstream ai:* node (auto-detected if omitted)Exits: rejected, approved
Example:
{ "nodeType": "review:content", "label": "Review Content", "config": {}}flow:wait
Section titled “flow:wait”Label: Wait | Category: flow | Credit cost: 0 cr
Pause lead progression for a configurable duration with jitter, or wait for an email open/click event
Config:
{ waitType?: "duration"|"email_event", durationMs?: number (ms, e.g. 86400000 = 1 day - required for duration mode), emailEventType?: "open"|"click"|"any" (default "any" - for email_event mode), timeoutHours?: number (default 72 - timeout before advancing) } - duration mode waits a fixed time (single exit). email_event mode has TWO exits: "triggered" (lead opened/clicked the email) and "timeout" (no engagement within timeoutHours). Pair email_event mode with google:email_send upstream.Example:
{ "nodeType": "flow:wait", "label": "Wait 3 Days", "config": { "waitType": "duration", "durationMs": 259200000 }}flow:if
Section titled “flow:if”Label: If / Condition | Category: flow | Credit cost: 0 cr
One-shot agent check of a condition (e.g., connection accepted, message replied)
Config:
{ conditionType: "connection_accepted"|"message_replied"|"custom", expression?: string }Exits: false, true
Example:
{ "nodeType": "flow:if", "label": "Check Connection Accepted", "config": { "conditionType": "connection_accepted" }}flow:filter
Section titled “flow:filter”Label: Filter | Category: flow | Credit cost: 0 cr
Gate lead flow based on rules (e.g., ICP score threshold, profile fields)
Config:
{ rules: Array<{ field, operator: "equals"|"contains"|"gt"|"lt"|"exists"|"not_exists", value }>, logic: "and"|"or" } - two exits: "pass" (rules matched) and "fail" (rules did not match). Leads on an unconnected exit stop there; an unlabeled edge is treated as the pass path (gate behavior).Exits: pass, fail
Example:
{ "nodeType": "flow:filter", "label": "Filter by ICP Score", "config": { "rules": [ { "field": "icpScore", "operator": "gt", "value": 50 } ], "logic": "and" }}enrich:wait
Section titled “enrich:wait”Label: Wait for Enrichment | Category: enrich | Credit cost: 0 cr
Park lead until profile enrichment completes, then proceed. Skips instantly if lead already has enrichment data.
Note: This node type is not available in the visual workflow builder. It is used internally by signal-agent workflows.
Config:
{ timeoutMinutes?: number (default 30) } - waits for lead enrichment to complete. Exits immediately if lead already has enrichment data. Timeout exit proceeds with whatever data is available.Exits: enriched, timeout
Example:
{ "nodeType": "enrich:wait", "label": "Wait for Enrichment", "config": { "timeoutMinutes": 30 }}flow:split
Section titled “flow:split”Label: Split | Category: flow | Credit cost: 0 cr
Distribute leads across 2-5 weighted branches for A/B testing. Assignment is deterministic - the same lead always takes the same branch.
Config:
{ branches: Array<{ label: string, weight: number }>, autoOptimize?: boolean, minLeadsPerBranch?: number } - 2 to 5 branches, weights must sum to 100. Exit handle IDs are branch indexes as strings ("0", "1", ...). When autoOptimize is true, a daily cron re-weights branches via Thompson Sampling on downstream positive exits once every branch has at least minLeadsPerBranch leads (default 20); lastOptimizedAt is set by the optimizer, do not set it manually.Example:
{ "nodeType": "flow:split", "label": "A/B Split", "config": { "branches": [ { "label": "A", "weight": 50 }, { "label": "B", "weight": 50 } ] }}notify:user
Section titled “notify:user”Label: Notify Me | Category: notify | Credit cost: 0 cr
Sends a notification to workspace members (in-app + email per their preferences) when a lead reaches this step - the message goes to YOU, never to the lead
Config:
{ subject: string, message: string } - both support {{variable}} interpolation: lead.firstName, lead.lastName, lead.company, lead.title, campaign.name, workflow.name, node.label. Unknown variables render empty. Recipients follow the workspace's workflow-notification audience setting (default: all members); there is no per-node recipient picker. Single exit, zero credits, never budget-gated.Example:
{ "nodeType": "notify:user", "label": "Notify Me", "config": { "subject": "{{lead.firstName}} {{lead.lastName}} reached {{node.label}}", "message": "{{lead.firstName}} {{lead.lastName}} ({{lead.title}} at {{lead.company}}) just reached {{node.label}} in {{workflow.name}}." }}integration:webhook
Section titled “integration:webhook”Label: Webhook | Category: integration | Credit cost: 0 cr
Sends lead data to an HTTP endpoint or pre-configured service (Slack, Instantly, etc.). Select a template for quick setup or use custom config with POST/PUT/PATCH and template variables.
Config:
{ url: string, method?: "POST"|"PUT"|"PATCH", headers?: Array<{ key, value }>, bodyTemplate?: string, blockOnFailure?: boolean, timeoutMs?: number } for custom webhooks, OR use a template: { templateId: "slack", templateFields: { webhookUrl: "https://hooks.slack.com/...", messageTemplate?: "mrkdwn text with {{lead:fullName}} etc." }, blockOnFailure?: boolean } or { templateId: "instantly-add-lead", templateFields: { campaignId: "uuid" }, blockOnFailure?: false }. Templates handle URL, method, headers, and body format automatically.Example:
{ "nodeType": "integration:webhook", "label": "Notify Slack", "config": { "templateId": "slack", "templateFields": { "webhookUrl": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL", "messageTemplate": "New lead: {{lead:fullName}} from {{lead:company}}" } }}enrich:company_info
Section titled “enrich:company_info”Label: Enrich Company Info | Category: enrich | Credit cost: 0 cr
Looks up the lead’s company details (industry, employee count, founding year, description, website, phone) through your connected data providers, trying each in your configured order until one finds a match
Config:
{ providers: Array<"apollo" | "clay">, clayRoutineId?: string (only used when clay is in providers; the Clay routine to run - copy its id from your Clay workspace, Clay's API cannot list routines; without it clay is skipped) } - providers are tried in order and only providers with a CONNECTED integration in this workspace are attempted; the first match wins. The lookup runs against the lead's linked company and fills in missing firmographics (industry, employee count, founded year, description, website, phone, location). Company data is shared, so a company already enriched with these fields is skipped (no repeat spend). Apollo resolves inline; Clay runs a routine and is polled, so the node can wait parked (up to 1 hour). Exits: enriched (company details found and written, or already present), not_found (no linked company or no provider matched).Exits: enriched, not_found
Example:
{ "nodeType": "enrich:company_info", "label": "Look Up Company", "config": { "providers": [ "apollo" ] }}enrich:contact_info
Section titled “enrich:contact_info”Label: Enrich Contact Info | Category: enrich | Credit cost: 0 cr
Finds the lead’s work email, and optionally personal emails and phone, through your connected data providers, trying each in your configured order until one finds a match
Config:
{ providers: Array<"apollo" | "instantly" | "clay">, revealEmail?: boolean (default true, includes personal emails), revealPhone?: boolean (default false, reveals the mobile/direct phone; apollo only), instantlyEnrichments?: Array<"work_email" | "profile"> (default ["work_email"]; only used when instantly is in providers; "work_email" finds the work email for 1.5 Instantly credits, "profile" fills headline/summary/title/company details from the lead's LinkedIn URL for 0.5 Instantly credits - it requires the lead to have a LinkedIn URL), clayRoutineId?: string (only used when clay is in providers; the Clay routine to run - copy its id from your Clay workspace, Clay's API cannot list routines; without it clay is skipped) } - providers are tried in order and only providers with a CONNECTED integration in this workspace are attempted; the first match wins. Apollo delivers revealed data by webhook, and Instantly and Clay are polled, so the node can wait parked (up to 1 hour) before routing; an async provider ends the cascade walk, so keep apollo before instantly or clay for email-first cascades. Exits: enriched (contact info found and written to the lead), not_found (no provider matched).Exits: enriched, not_found
Example:
{ "nodeType": "enrich:contact_info", "label": "Find Work Email", "config": { "providers": [ "apollo" ], "revealEmail": true }}integration:add_to_sequence
Section titled “integration:add_to_sequence”Label: Add to Sequence | Category: integration | Credit cost: 0 cr
Hands the lead off to a connected outreach tool’s sequence (Apollo sequence or Instantly campaign), or chains the lead into another CampaignStack workflow
Config:
{ provider: "apollo" | "instantly" | "campaignstack", campaignId: string (the provider-side sequence/campaign id; for campaignstack it is the TARGET WORKFLOW id in the same workspace, never this workflow), emailAccountId?: string (provider-side sending mailbox id, REQUIRED for apollo, unused otherwise), activateSequence?: boolean (apollo only: start the sequence after adding, best-effort), skipIfInWorkspace?: boolean (instantly only: skip leads whose email already exists anywhere in the Instantly workspace, default false), blockOnFailure?: boolean (default false) } - for apollo/instantly, leads without an email are skipped gracefully and duplicate handoffs are idempotent successes. Apollo requires a master API key; Instantly needs no mailbox or activation. For campaignstack, the lead enters the target workflow at its entry node, no email or integration connection is needed, and each lead enters a given workflow AT MOST ONCE EVER (repeat handoffs report already present, they never re-run the lead - this makes workflow chains loop-safe). Single exit.Example:
{ "nodeType": "integration:add_to_sequence", "label": "Add to Apollo Sequence", "config": { "provider": "apollo", "campaignId": "your-sequence-id", "emailAccountId": "your-email-account-id" }}integration:add_to_list
Section titled “integration:add_to_list”Label: Add to List | Category: integration | Credit cost: 0 cr
Adds the lead to a lead list: one of your own CampaignStack lists, a connected email tool’s lead list (Instantly), or a Clay table - no outreach starts either way
Config:
{ provider: "instantly" | "clay" | "campaignstack", listId: string (instantly: the provider-side lead list id; clay: a registered Clay table id from the integration - the user registers tables with their webhook URLs on the Integrations page; campaignstack: one of your own lead list ids - a user-visible set list, never a workflow node list), skipIfInWorkspace?: boolean (instantly only: skip leads whose email already exists anywhere in the Instantly workspace, default false), blockOnFailure?: boolean (default false) } - duplicate adds to the same list are idempotent successes, never errors. For instantly, leads without an email are skipped gracefully and no integration events flow back. For clay, the push is fire-and-forget into the table's webhook source: leads with an email OR a LinkedIn URL qualify, a campaignstack_dedupe_key field rides along for table-side dedup, and no result id comes back. For campaignstack, no email and no integration connection are needed; the lead lands in your own list (other workflows sourcing from that list will pick it up). Unlike Add to Sequence, no outreach starts. Single exit.Example:
{ "nodeType": "integration:add_to_list", "label": "Add to Instantly List", "config": { "provider": "instantly", "listId": "your-lead-list-id" }}integration:crm_sync
Section titled “integration:crm_sync”Label: CRM Sync | Category: integration | Credit cost: 0 cr
Upserts the lead as a contact in your connected CRM (e.g. HubSpot), optionally linking their company and logging the latest outreach as a note
Config:
{ provider: "hubspot", syncCompany?: boolean (default true - associate the lead's company when a real domain resolves), logNote?: boolean (default true - log the latest outreach message as a timeline note), lifecycleStage?: string (NEVER sent unless explicitly set - downgrade risk on existing CRM contacts), blockOnFailure?: boolean (default false) } - leads without an email are skipped gracefully (upsert is email-keyed). Single exit.Example:
{ "nodeType": "integration:crm_sync", "label": "Sync to HubSpot", "config": { "provider": "hubspot", "syncCompany": true, "logNote": true }}