Define Sequence
define_sequenceAuthor (or replace) the campaign's flow sequence — a DAG of outreach nodes. Each node
carries its own out-edges as typed fields, so the grammar is the schema: a connection
request has accepted / already_connected / timeout slots, a send has replied /
follow_up, a silent action and a manual step each have then, a decision has arms, a
terminal has none.
Derive the DAG from the agent's goal + triggers: one node per step, each natural-language
fork a decision, and terminal only for resting end-states (auto-skip, human handoff,
done). The flow must be acyclic, every node reachable from start, and every path
capped by a terminal (only a terminal may dead-end) — all enforced.
The shape:
start(exactly one, the DAG head):{id, label, entry}. Holds prospects no send has reached (label it "Not started").entryis the single first touch —{to}for an immediate open, or{to, after: {value, unit}}(units w/d/h/m) to hold that long from when each prospect is tracked. The flow fires it on its own: do NOT queue the first sends yourself on a sequenced campaign — track prospects and let the flow open.connection_requestnode (LinkedIn):{id, label, kind, accepted, already_connected, timeout?, message_templates?}.message_templatesholds its note (see below); without one the step'son_entercode or instructions decide (its minted default code sends no note). Routes BOTH send outcomes —accepted(invite taken) andalready_connected(a CR onto an existing 1st-degree connection, a no-op), each a target node id; both required. Routealready_connectedto aterminalby default (surface the existing relationship for opt-in rather than cold-messaging it); point it at the message step only when the user says to reconnect with existing connections.timeoutis an optional held advance{delay: {value, unit}, to}(e.g. withdraw after no accept).sendnode (a repliable send — LinkedInmessage/inmail, or anemail):{id, label, kind, channel, action, replied, follow_up?, message_templates?}.channelislinkedinoremail(so one sequence spans both);actionis a LinkedIn verb for channel linkedin oremailfor channel email. Routesreplied(required) so a reply advances the prospect off the node;follow_upis an optional held no-reply follow-on{delay, to}.message_templatesholds the step's message (see below).actionnode (a bodyless LinkedIn action — follow / withdraw / resolve / comment / reaction):{id, label, kind: "action", channel: "linkedin", action, then}. Nothing to reply to;thenis its single advance —{to}fires the instant the action's send lands (a bodyless step flows straight into the next node with no hold), or{to, after}to hold first. Aresolveaction may addnotify: true— the visible "View Profile" visit (LinkedIn notifies the person you viewed them), a warm-up touch typically placed later; the default silent resolve just reads profile data to branch on.manualnode (a step a HUMAN performs off-platform that the system can't enact — a call, a gift, a recorded note):{id, label, kind: "manual", action_description, then}. No channel.action_descriptionis the fully-authored ask (no send is composed, no agent run boots — the step is purely a note for the user); entry queues it onto the user's home approval list.thenis the next node's id (a bare string, not a{to}object — unlike a silent action'sthen): the prospect advances onto it when the user marks the queued step done, with no timed hold (completion is human-paced). UsemanualONLY when a human must act — never for a step the agent can do itself (that step never happens, since a manual node boots no run): an automatable side-effect (update a CRM, create a task in a connected tool, post a notification) is anautomationnode, and an automatable outreach touch is anaction/sendnode.automationnode (an automated non-send side-effect the AGENT performs — update a CRM record, create a HubSpot task, post a Slack/webhook notification, tag a record):{id, label, kind: "automation", then}. No channel.thenis the next node's id (a bare string, like a manual node's). Author the side-effect itself onto this node's on_enter trigger withupdate_trigger(aprompt, orcodefor a deterministic one) — the same way a terminal node's hook is authored, not a field here; the enactment run carries it out for each prospect then advances. Unlikemanualit boots an agent run and needs no human; unlike aterminalhook it advances. Reach for this when the user wants a step that "does something" automatically mid-sequence and then continues.decisionnode:{id, label, kind, rule, arms}.ruleis the NL fork judgment;armsis a list of{case, to}— eachcasea non-blank condition, the default authored as one too (e.g.case: "else"). A decision has no timed edge and no bare advance — only arms. A case arm may target any enactable node — a send / action / automation step, another decision, or a manual step — or a terminal: the decision run moves each prospect onto its arm, and entering it fires that node's own on_enter enactment on its next run (a send/action composes its send, a nested decision judges its fork, an automation runs its side-effect, a manual step queues the human's task), never an inline send. Chain a downstream decision when several arms share one judgment (DAG reuse); use compound arms on one rule ("A and B" / "A and not B" / "else") when one prospect needs several independent conditions judged together; target a manual step to hand a judged arm off to a human.terminalnode:{id, label, kind}. A resting end-state, no out-edges.
Every timed hold — a start after, a connection_request timeout, a send follow_up, a
silent action's held then — is {value, unit}. A d (day) unit counts BUSINESS days —
Mon–Fri, skipping Saturday/Sunday — and is the DEFAULT for a day-count delay: a plain
"follow up in 3 days" (like "3 business days") is {value: 3, unit: "d"}. Reach for cd
(calendar day) ONLY when the user explicitly wants calendar days including weekends.
w/h/m (week/hour/minute) are always calendar.
Entering a node fires its enactment: a connection_request / send / action node
composes and enqueues its step, a manual node queues its step onto the user's
approval list, an automation node runs its authored side-effect and advances, a
decision judges its fork and routes each
prospect. Run a second channel alongside the first by chaining it
off a step's held advance (a CR timeout, a send follow_up, or a silent action's held
then) — not a second start entry: a prospect holds one flow position, so start takes
exactly one entry.
A send node's message is its message_templates: a one-entry list holding the message itself, with each
personalized part a {slot} — a named field ({first_name}, {company}) or a described
slot ({a line about their recent post}); the enactment run fills every slot per prospect
and, unless the step's prompt lets it rephrase, keeps the rest of the wording. The
templates are saved with the step and come back on get_campaign_flow; omit
message_templates to leave a step's templates as they are, or pass [] to remove them.
To A/B test a step's message, add a second version to the list: prospects
reaching the step alternate between the two, and get_message_experiments reports which gets
more replies. At current volumes a test takes weeks and only separates meaningfully
different messages (a different opener or ask, not a reworded phrase), so propose versions
that differ that much. Changing either version's text restarts the test. To end a test,
pass just the version you keep.
A connection_request node's message_templates holds its note the same way (at most 300
characters, filled per prospect), where "" is a version sent with no note: ["", note] A/B
tests a note against none, and get_message_experiments reports which gets more requests
accepted.
Anything else the run should know for that step (tone, who to mention, what to skip,
whether it may rephrase the template, what to write when a slot can't be filled) goes in
the step's on_enter trigger prompt via update_trigger — keep
the message itself in message_templates, not in that prompt or in workspace.inputs, so
the user sees and edits it on the step. When you first set a template on a step whose
prompt already carries its message, rewrite that prompt down to the extra instructions.
(workspace.inputs stays the home for a flowless
campaign's templates and for per-agent data no single step owns — criteria, audience,
exclude lists.)
Authoring (or replacing) a sequence on an agent that ALREADY holds untouched prospects enrolls that whole backlog into it — so before defining a sequence on an agent with an existing roster (a monitoring or engager-capture agent especially), confirm with the user that the accumulated prospects should all enter the campaign.
Re-authoring replaces the prior sequence (allowed on a live campaign); a prospect still
resting on a dropped node returns under off_sequence_nodes (with a note on re-homing)
rather than being lost. The node-owned on_enter triggers reconcile in the same call:
every node kept in the new DAG (same id, any kind) keeps its on_enter row and the
prompt/code on it, a new enactable node gets a fresh row auto-minted, and only a node
dropped from the DAG entirely has its on_enter row deleted. A new resolve /
connection_request / follow node is minted with a default deterministic enactment code (it
enacts clean with no agent run — edit it for a non-standard step; a connection request's note
goes in its message_templates, not the code); every other
kind mints code-less. A reused id keeps its existing row — so after changing what that id's node
does, review its on_enter content; an unmodified minted default is regenerated for the new
kind automatically, but an edited one is left as you wrote it.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| agent_id | Yes | ID of the agent to attach the sequence to. | |
| sequence | Yes | The flow DAG (typed per-kind nodes). Validated for structural integrity, reachability, and acyclicity. |