Campaigns
Multi-step automated subscriber journeys: immutable versions, entry triggers, graph nodes, preflight, publishing, per-version reports, and one journey's trace.
本頁內容
A campaign is a multi-step automated subscriber journey: entry on a trigger, then a sequence of nodes (messages, delays, conditions, experiments) that can stretch over days or weeks. It is not the same thing as a broadcast or an email campaign , those send one content document in one wave; a campaign is a branching graph that reads and changes subscriber data at every step.
Versions and the draft
A campaign always has exactly one editable draft, and, once it has ever been
published, a history of immutable versions: draft → published → retired.
The draft carries the trigger, audience, settings, and node graph. Editing the
draft goes through PUT /draft with an expected_revision: if someone else
already saved the draft first, the request is rejected as a conflict instead
of silently overwriting their edit.
Publishing (POST /publish) freezes the current draft as a new published
version and immediately opens the next draft as its copy. A subscriber who
entered while version N was published walks their whole path on version N,
even after version N+1 is published later: the trigger, audience, settings,
and graph an already-running journey is executing never change under it.
The node graph can only be edited while the campaign is stopped: saving a graph while the campaign is Running or Paused is refused: a subscriber may be sitting on exactly that node by its id, and the executable graph must not change under a live subscriber.
Separate from versions, there is cloning: any existing version: a draft, a published one, or a retired one: can be opened as a new draft, replacing whichever draft is currently open. A campaign can also be archived (taken off the active list without deleting its history), but only while stopped.
Entry triggers
- Manual: no configuration; the editor's "send a test to myself" button takes the same path and runs the entry node immediately, without waiting for the background sweep.
- Site event: the same client SDK/widget event as in reactions, with the same condition editor.
- Schedule: once, at a fixed instant, or recurring. A recurring schedule
is a local time of day (
HH:MM) in an explicit IANA timezone: "Local" is refused: an optional weekday filter, and mandatorystart_at/end_atno more than two years apart: a recurring schedule must have an end, or a forgotten campaign would sweep the audience forever. A wall-clock moment that does not exist because of a spring-forward transition is simply absent from the schedule by default; the "next valid instant" policy moves it to the end of the gap instead of skipping it. - Label changed: one specific label added or removed, never both at once : those are different journeys with different content.
- Property changed: a transition of one approved profile property from one state to another. The "previous" and "new" conditions are independent and both optional; re-saving the same value is not a transition and never triggers entry.
- Segment entered: a membership transition from "not a member" to "member" of a ready segment. An ordinary recompute that merely reconfirms existing membership does not count as entry.
Graph nodes
The graph is nodes and edges, and an edge carries a branch label (for example
yes/no). Exactly one node has no incoming edges: the entry point; terminal
nodes ("Goal", "Exit") have no outgoing edges, and every node must have a path
to at least one terminal: a graph with an unreachable node or a node with no
path to a finish will not save. One node's configuration is capped at 32 KiB;
a campaign as a whole is capped at 200 nodes and 400 edges.
- Message: text/photo/file/delay blocks, run as an ordinary reaction against the same subscriber. AI blocks, a nested reaction, "expected answer" state, and parameter writes with a side effect are not supported here: retrying the node after a failure has to stay safe.
- Content: unlike "Message", sends one specific published, version- pinned content document on one explicitly named channel (Telegram, VK, WhatsApp, or web chat), and never switches channel by itself: an unreachable recipient either takes a separate "when unavailable" branch, if one is wired, or the node fails visibly.
- Delay: waits a fixed number of seconds (up to 180 days).
- Wait for event: waits for a client event by name (up to 64 characters,
no reserved
$prefix) with a timeout of up to 180 days; the node has exactly two branches: the default one (the event arrived) andtimeout. - Condition: branches
yes/noon subscriber parameters, time periods, time since last seen, or message count: the same condition vocabulary as reactions. - Split: a weighted A/B branch: a subscriber lands in the same branch deterministically, by a hash of the node and their own id, and that does not change on re-evaluation or restart.
- Experiment: branches on a shared experiment's variant, with a pinned experiment id and version; two to sixteen variants, and every declared variant is a mandatory branch: an unwired variant would silently strand whoever the experiment assigns to it.
- Channel available: branches
yes/noon whether the subscriber can actually be reached right now on one explicitly named channel (email and phone do not qualify: neither is a delivery channel). It exists so an author decides the fallback channel question explicitly, rather than relying on a send silently switching channel on its own. - Action: change labels; unset parameters (setting, appending, and
incrementing are not supported here: the same retry-safety principle as
"Message"); a synchronous web request (no async mode, no nested reaction on
success/error; the
Idempotency-Keyheader is reserved: the platform sets it itself and sends the same value when the same step is retried, so your endpoint can use it to drop duplicates, and you cannot set your own); sync to a connected CRM; assign the support dialog to an agent or a group; emit a customer event (the same as a widgettrack()call: segments, funnels, and a matching trigger will see it); start a lead bot. - Goal and Exit: terminal nodes with no configuration: the first is a successful finish, the second is leaving without one.
Preflight and publishing
POST /preflight checks the current draft's shape (the same graph validator
that runs on every save) and its cross-entity references: do the named
segments, content documents, and experiments actually exist: and sends
nothing. The response is ready, the version number, its checksum, and a
list of errors with a code (invalid_graph, invalid_reference) and a
message. Preflight does not estimate audience size, channel eligibility, or
consent: that is out of scope for it.
POST /publish re-runs the same reference checks again, in one transaction: a
client that skipped preflight, or whose world changed since, still cannot
publish a campaign pointing at a deleted segment or document. Publishing
requires the draft's current revision: a stale revision or an already-
published draft returns a conflict with no partial result.
Per-version reports and the journey trace
The report (GET /report) is computed per version, not for the campaign
as a whole: version N and version N+1 carry different counters, and
publishing a new version never shifts an old one's numbers. The report has a
journey funnel (entered, active, waiting, on retry, done, exited, failed) and,
per node, entered/completed/failed counts. A report is sealed once its
version can no longer take a new entrant and no journey on it can move any
further: those numbers are stored as final and never recomputed; a live
published version's report is always recomputed on read.
One journey's trace (GET /journeys/{subscriberID}/{runNo}) is a bounded
event log: entering a node, an effect firing, parking on a wait, leaving a
wait, a retry being scheduled, giving up after exhausting attempts, finishing.
The trace carries ids, branches, attempt counts, and error codes: never a
message body or a provider response: it is an audit of the path taken, not a
copy of what was sent. A truncated flag shows a journey exceeded the event
cap and part of its history was not kept. A separate list
(GET /failures) shows who is stuck, and on which node, after the runner
exhausted its attempts; a stuck journey can be retried
(POST /journeys/{subscriberID}/{runNo}/retry).
Quiet hours
A version has a free-form settings blob, seeded by default as
{"allow_reentry": false, "timezone_mode": "bot", "quiet_hours": {"enabled": false}}.
Re-entry (allow_reentry) is a separate, genuinely enforced field
on the campaign itself (not on version settings): it decides whether one
subscriber may enter the campaign a second time. timezone_mode and
quiet_hours inside settings, however, are currently a reserved
configuration shape only: the campaign runner does not read or apply them.
That is what sets them apart from the same-named feature in
Web Push, where quiet hours genuinely defer delivery. Do not
assume a campaign will wait for a "convenient" time on its own: model a
pause with a "Delay" node or a time-period condition instead.
Permissions
Personal tokens and roles see campaigns through three separate scopes:
campaigns:read, campaigns:write, and campaigns:publish: publishing a
version and retrying a stuck journey are kept apart from ordinary draft
editing. All three require the "Reactions" bot right: without it, the
campaigns section is neither visible nor reachable for reading or writing.
What's next
- Broadcasts: a one-off send of a single document, with no graph and no versions.
- Email campaigns: a similar one-off scenario for email.
- Experiments and attribution: the shared experiment an "Experiment" node can reference.
- People (CRM): segments and labels for audience and triggers.
- Content Studio: documents for the "Content" node.
- REST API and Tokens: campaign routes and their scopes.