Nodes
The graph a flow draft carries: how nodes chain through nextStep and paths, why relations are bare ids, the node types the API accepts, how a send is written (sender, text, consent policy, translations, RCS fallback), and every branch.
A flow draft is a single JSON object, { "trigger": { ... } }, with the whole graph nested inside the trigger. Every node is an object whose stepClass names its type. It is the same shape the draft read endpoint returns and every template payload carries, so the most reliable way to write one is to start from a template and change it.
This page covers what comes after the trigger. The trigger itself, its exits and its re-entry settings are in Triggers and exits; the five kinds of wait are in Delays and waits.
How nodes chain
- A sequential node continues on
nextStep, one node object. A node with nonextStepends that line. - A branch node has
pathsand, on most branches, adefaultPath. Each path is{ "name"?, "nextStep", ...its condition }. When a path's chain ends, the contact continues on the branch's ownnextStep, shared by every path; a path with nonextStepgoes straight there. - There are no merges or loops. The graph is a tree, and a node reachable from more than one place is a validation error.
- Exits are not nodes in the chain. They live in the trigger's
exitSteps. uidis optional on every node and path. Keep the one you read so the node keeps its identity across saves; omit it on nodes you add.
{
"trigger": {
"stepClass": "TRIGGER_CONTACT_EVENT",
"eventType": "update",
"eventDatasource": null,
"eventQueryFilter": { "root": { "type": "group", "join": "and", "children": [
{ "type": "event_condition", "key": "update.new-contact", "operator": "matches-bool", "values": [true] }
] } },
"maxActivationsPerContact": 1,
"nextStep": {
"stepClass": "ACTION_BRANCH_SMART_DELAY",
"minDelaySeconds": 300,
"maxDelaySeconds": 86400,
"preset": "recommended",
"urgency": "balanced",
"legalHours": true,
"paths": [
{
"outcome": "waitComplete",
"nextStep": {
"stepClass": "ACTION_SEQUENTIAL_SEND",
"channelType": "sms",
"channel": {
"channelType": "sms",
"compliancePolicy": "no-opt-out",
"options": [
{ "sender": "67bdfa983114d0062d733795", "template": "Welcome, {{_first_name}}! Unsubscribe: {{unsubscribe}}", "allowUnicode": false }
]
}
}
},
{ "outcome": "cannotSatisfy" }
]
}
}
}Relations are bare ids
A node that references something else — a sender, a data source, a segment — takes the bare id string, never an object such as { "id": "..." }. The read shape of some other endpoints uses objects; the draft does not (an object carrying an id is tolerated and flattened, but write the string).
Every reference must belong to this project: sender ids from GET /channel/sms/sender or GET /channel/rcs/sender, data source ids from the event volumes probe, segment uids from GET /segment. An id that does not exist is a 422; one that exists in another project or organization is a validation error on the draft. Copy ids from those responses; never retype or guess them.
The nodes the API accepts
Only these stepClass values may be placed through the API. A draft containing any other is refused with a 400 before anything is saved, and the message lists the allowed ones.
| Kind | stepClass |
|---|---|
| Trigger and exits | TRIGGER_CONTACT_EVENT, ACTION_SEQUENTIAL_EXIT_TRIGGER_EVENT |
| Messages | ACTION_SEQUENTIAL_SEND |
| Contact changes | ACTION_SEQUENTIAL_CONTACT_TAG, ACTION_SEQUENTIAL_CONTACT_LIST, ACTION_SEQUENTIAL_SUBSCRIPTION_MANAGE |
| Outcomes | ACTION_SEQUENTIAL_GOAL, ACTION_SEQUENTIAL_END |
| Waits | ACTION_SEQUENTIAL_DELAY, ACTION_BRANCH_TIMEZONE_DELAY, ACTION_BRANCH_SMART_DELAY, ACTION_BRANCH_CONTACT_EVENT_WAIT, ACTION_BRANCH_MESSAGE_DELIVERY_WAIT |
| Branches | ACTION_BRANCH_CONTACT_CONSENT, ACTION_BRANCH_CONTACT_TAG, ACTION_BRANCH_CONTACT_LIST, ACTION_BRANCH_CONTACT_SEGMENT, ACTION_BRANCH_CONTACT_ATTRIBUTE, ACTION_BRANCH_TRIGGER_EVENT, ACTION_BRANCH_CONTACT_EVENT_SEARCH, ACTION_BRANCH_CHANNEL_CAPABILITY, ACTION_BRANCH_ABTEST |
The dashboard has a few more — a delay where a model picks the moment, and a trigger that fires on a schedule — which cannot be placed through the API. A flow built in the dashboard with one of them can be read, but a replace that carries it back is refused, so leave such flows to the dashboard.
The send
ACTION_SEQUENTIAL_SEND sends one SMS or RCS message and moves on at once: it has no success or failure path. To act on whether the message arrived, follow it with a delivery wait.
channelTypestringrequiredsms or rcs.
abortFlowIfUnableToSendbooleandefault: trueWhen nothing can go out to this contact (no phone, no consent, a blocked number), true ends the run here and false lets it continue.
channelobjectrequiredThe message.
channelTypestringrequiredRepeats the node's channelType.
compliancePolicystringThe consent policy this send applies: no-opt-out for marketing journeys, opt-in to reach only contacts with explicit consent, basic for a message that confirms what the contact just did. The values mean what they mean on a campaign — see Consent policy. Always set it: left out, the send also reaches contacts who refused marketing, and the draft advice flags it.
optionsobject[]requiredExactly one entry.
senderstringThe id of a sender of this project for the channel. A send without one is saved but does not validate.
templatestring | objectThe message. For SMS, the text, with template variables such as {{short:...}} and {{unsubscribe}}, plus {{_first_name}} and {{_event.<parameter>}}, which reads a parameter of the event that triggered the flow. For RCS, an RCS message object: a plain-text one is { "text": { "text": "..." } }, never a bare string.
allowUnicodebooleanSMS only. Allows non-GSM characters, which lower the characters per billed part.
localizationsobject[]Translations. See Translations.
fallbackobjectRCS only. See Fallback.
Translations
An option may carry localizations: [{ "language", "template" }]: the same message in other languages. At send time each contact receives the translation matching their language, and anyone else receives the option's own template. A translation uses the option's sender; leave its sender out and it takes it on save, so setting the option's sender is enough. Multi-language messages depend on the organization's plan; template payloads only carry a translation when the plan allows it.
Fallback
An RCS channel may carry fallback: another channel object — in practice an sms one, with its own sender and text — sent when RCS cannot reach the contact. It applies the parent's consent policy.
- An SMS channel cannot have a fallback, and a channel cannot fall back to its own type.
skipFallback: trueswitches the fallback off without deleting it.sendUnsupported(defaulttrue) also sends RCS to contacts whose RCS support is unknown.
With channel=rcs, the template list builds every send this way: a plain-text RCS message with the original SMS as its fallback. Both levels need a sender.
Sequential nodes
All continue on nextStep.
stepClass | What it does | Fields |
|---|---|---|
ACTION_SEQUENTIAL_CONTACT_TAG | Adds or removes tags on the contact, as a manual edit would. | mode (add, the default, or remove), tags (at least one). |
ACTION_SEQUENTIAL_CONTACT_LIST | Adds the contact to lists or removes them. | mode, lists (at least one). |
ACTION_SEQUENTIAL_SUBSCRIPTION_MANAGE | Sets one consent state, as a manual change would. | channel (sms, which also covers RCS), operation (opt-in, opt-out, suppress or reactivate). |
ACTION_SEQUENTIAL_GOAL | Marks the run as having reached the flow's goal, once. Sends nothing, stops nothing. See Goals. | — |
ACTION_SEQUENTIAL_END | Ends the run here, without continuing on an enclosing branch's nextStep. | No fields, no nextStep. |
ACTION_SEQUENTIAL_DELAY | A fixed wait. See Delays and waits. | delaySeconds. |
Branches
Fixed outcomes
The node decides which paths exist; you fill each path's nextStep.
ACTION_BRANCH_CONTACT_CONSENTroutes by whether the contact would pass a consent check:channelTypes(sms,rcs),channelMatchMode(allorany) andcompliancePolicy.pathsholds exactly one"outcome": "allowed"and one"outcome": "blocked", and there is nodefaultPath. It routes, it does not replace the send's own policy. To split a journey by consent, use this branch rather than a segment.ACTION_BRANCH_TIMEZONE_DELAYandACTION_BRANCH_SMART_DELAY: onewaitCompleteand onecannotSatisfypath. See Delays and waits.ACTION_BRANCH_MESSAGE_DELIVERY_WAIT: paths by delivery outcome. See Waiting for delivery.
Conditions
paths are evaluated in order and the first that matches wins; defaultPath, which is required and carries no condition, takes everyone else.
stepClass | Each path | Notes |
|---|---|---|
ACTION_BRANCH_CONTACT_TAG | { "mode": "oneOf" | "noneOf", "values": [...] }, values being tag names | Reads the contact as it is now. |
ACTION_BRANCH_CONTACT_LIST | Same, with list names | Reads the contact as it is now. |
ACTION_BRANCH_CONTACT_SEGMENT | Same, with segment uids | A segment may lag the contact by a few seconds. |
ACTION_BRANCH_CONTACT_ATTRIBUTE | filter: an audience query filter over the contact | For what the three above do not cover. |
ACTION_BRANCH_TRIGGER_EVENT | filter: a filter over the event that started the run | |
ACTION_BRANCH_CONTACT_EVENT_SEARCH | eventType, optional eventDatasource, withinDays, sinceEntry (default true: only since the contact entered the flow) and filter | Did an event happen. |
ACTION_BRANCH_CHANNEL_CAPABILITY | states, from supported, notSupported and unknown, for the node's channel (sms or rcs) | Reads stored reachability; the send decides again anyway. |
The branch that pauses until the contact acts, ACTION_BRANCH_CONTACT_EVENT_WAIT, is a wait as much as a branch: see Waiting for an event.
Random split
ACTION_BRANCH_ABTEST splits contacts at random. Each entry of paths has an integer percentage, and defaultPath carries the remainder as its own percentage. The paths must leave the default a share.