Skip to content

Build a Campaign over the API

This guide assembles a complete campaign through the API without any server-side LLM call: a campaign, its ICP, a persona, a roadmap phase and a workflow. You supply the data and the platform stores it, so none of these steps spends credits.

It takes three tool calls: campaignstack_edit_campaign twice and campaignstack_edit_workflow once.

  • An API key with campaigns:write and workflows:write, plus campaigns:read and workflows:read to read results back. See Scopes.
  • Your workspaceId: call campaignstack_whoami. With a workspace key (cs_) you can omit it everywhere.

Every edit_* tool takes an operations array and applies each operation independently. The response carries one result per operation:

{
"ok": true,
"workspaceId": "ws_abc123",
"results": [
{ "ok": true, "operation": "create", "value": { "campaignId": "camp_xyz789", "title": "Series B Fintech Founders — Q3" } }
]
}

A failed operation has "ok": false and an error string, and the others still apply. Check every result, not just the top-level ok.

  1. Create the campaign

    campaignstack_edit_campaign, op create, scope campaigns:write

    {
    "workspaceId": "ws_abc123",
    "operations": [
    {
    "op": "create",
    "title": "Series B Fintech Founders — Q3",
    "goal": "Book discovery calls with CTOs at fintech companies that raised Series B in the last 18 months",
    "description": "Outbound sequence targeting warm fintech leads via LinkedIn"
    }
    ]
    }

    Take results[0].value.campaignId for the next steps.

  2. Add the ICP, a persona and a phase in one call

    campaignstack_edit_campaign, ops icp, persona and roadmap

    A campaign has exactly one ICP: the icp op creates it the first time and updates it after that. Personas attach to that ICP automatically.

    {
    "workspaceId": "ws_abc123",
    "operations": [
    {
    "op": "icp",
    "campaignId": "camp_xyz789",
    "data": {
    "titles": ["CTO", "VP Engineering", "Head of Technology"],
    "industries": ["Financial Services", "Fintech"],
    "seniorities": ["Director", "VP", "C-Suite"],
    "companySizes": ["11-50", "51-200", "201-500"],
    "keywords": ["Series B", "payments", "banking", "embedded finance"]
    }
    },
    {
    "op": "persona",
    "campaignId": "camp_xyz789",
    "data": {
    "name": "Technical Co-founder",
    "description": "CTO or VP Eng at a recently funded fintech. Owns the build-vs-buy decision for infrastructure.",
    "painPoints": ["Scaling payment rails without adding headcount"],
    "goals": ["Ship faster without sacrificing reliability"],
    "objections": ["We already have a vendor for this"]
    }
    },
    {
    "op": "roadmap",
    "campaignId": "camp_xyz789",
    "data": {
    "name": "Initial Outreach",
    "description": "First-touch LinkedIn connection sequence targeting warm leads.",
    "objectives": ["Connect with 50 qualified leads per week"],
    "phaseType": "outreach",
    "order": 0
    }
    }
    ]
    }

    Every ICP criteria field is optional. Personas are optional too, but they improve the drafts campaignstack_craft_message writes later. Valid phaseType values: sourcing, outreach, content, intelligence, refinement.

    The icp op schedules a rescore of leads that were already scored. To also score leads that never were (for example right after an import), add "rescore": true to the icp operation.

    To keep the campaign fed from LinkedIn posts on a subject, add a search_watch operation. It searches LinkedIn posts on one taxonomy topic about every six hours and brings their authors into the campaign. Take the topic from include.topicVocabulary of campaignstack_get_campaigns. accountIds is optional and picks which of the workspace’s LinkedIn accounts run the search:

    { "op": "search_watch", "campaignId": "camp_xyz789", "data": { "name": "fintech", "accountIds": ["li_acc_123"] } }

    One watch per topic runs in a workspace. When another campaign already watches the topic, the result comes back with "enabled": false, a disabledReason, and that campaign in runningOn; this watch starts when the other one stops. Send the same operation with "enabled": false in data to stop a watch.

  3. Create the workflow

    campaignstack_edit_workflow, op create, scope workflows:write

    First call campaignstack_get_workflow_node_types (no arguments). It returns every node type with its configHint, the graph shape and an example graph. It is the authoritative source: node types are versioned and can change.

    Then create the workflow with a graph and a source configuration:

    {
    "workspaceId": "ws_abc123",
    "operations": [
    {
    "op": "create",
    "campaignId": "camp_xyz789",
    "name": "LinkedIn Connection Sequence",
    "sourceConfig": {
    "providers": [
    {
    "id": "default",
    "type": "campaign_pool",
    "priority": 0,
    "enabled": true,
    "config": { "scoreMin": 70, "rules": [], "logic": "and" },
    "state": { "leadsSourced": 0, "exhausted": false, "dailyCapSourced": 0 }
    }
    ],
    "processingOrder": "cascade",
    "globalDailyCap": 20,
    "exclusions": []
    },
    "graph": {
    "nodes": [
    { "nodeType": "linkedin:profile_view", "label": "View Profile", "config": {} },
    {
    "nodeType": "flow:wait",
    "label": "Wait 2 days",
    "config": { "waitType": "duration", "durationMs": 172800000 }
    },
    {
    "nodeType": "linkedin:connection_request",
    "label": "Send Connection Request",
    "config": { "noteMode": "none" }
    }
    ],
    "edges": [
    { "sourceIndex": 0, "targetIndex": 1 },
    { "sourceIndex": 1, "targetIndex": 2 }
    ]
    }
    }
    ]
    }

    This sourceConfig is the platform default: one Campaign Pool source feeding the campaign’s leads that score at least 70 against its ICP, at most 20 new leads a day. Do not include the source:entry anchor node in nodes; the platform creates it.

    For a note on the connection request, use "noteMode": "static" with a noteTemplate such as "Hi {{lead:firstName}}, ...". For a personalized AI note, chain ai:craft_note → review:content → linkedin:connection_request with "noteMode": "static"; the configHint of each node explains the wiring.

    New workflows start paused.

  • Import leads: campaignstack_import_leads (parsed CSV rows, up to 100 per call) or campaignstack_import_external_leads (Apollo, Instantly or HubSpot).
  • Score them: run the icp op again with "rescore": true.
  • Start the workflow: campaignstack_edit_workflow with op set_enabled and "enabled": true.
  • Monitor: campaignstack_get_workflows with include.stats for per-node counts, and include.nodeLeads (with nodes.ids) to see who is at a node. campaignstack_diagnose_workflow_sourcing explains why a given lead did or did not enter.
  • Change the graph later: campaignstack_edit_workflow_graph, in patch or replace mode.
  • Send feedback: campaignstack_submit_feedback reports issues or requests features directly from your agent.

For event-driven outreach (engage people who just changed jobs, or who engaged with a post), create a signal agent with campaignstack_edit_signal_agent, mode create. Signal agents watch LinkedIn and enqueue matching leads into their own workflow. They are separate from the campaign sequence above; see Signal Agents & Watches.

  • A rejected graph comes back with the reason. campaignstack_get_workflow_node_types shows the exact config shape each node expects.
  • An operation that fails leaves the others applied. Fix it and send only that operation again.
  • campaignstack_submit_feedback with category: "bug" or category: "feature" goes straight to the CampaignStack team.