# 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.

**Language:** en
**Audience:** developer
**TLDR:** A draft is { trigger: {...} } with the graph nested inside. Sequential nodes continue on nextStep; branches have paths (and usually a defaultPath) that rejoin on the branch's own nextStep; the graph is a tree. Relations are bare ids of this project's senders, data sources and segments. Only the stepClass values listed here are accepted; the AI delay and the schedule trigger are refused with a 400. Every send needs a sender, a text and a compliancePolicy.
**Search keywords:** node, nodes, step, steps, stepClass, graph, flow graph, export shape, nextStep, paths, defaultPath, branch, branches, uid, relation, bare id, ACTION_SEQUENTIAL_SEND, send step, sender, compliancePolicy, consent policy, no-opt-out, localizations, translation, multi-language, RCS fallback, SMS fallback, skipFallback, tag contact, add to list, subscription manage, opt-out step, goal node, end node, A/B test, split test, consent branch, segment branch, attribute branch, channel capability, allowed node types, AI delay, not allowed
**Related pages:** /developers/product-api/flows/triggers-and-exits, /developers/product-api/flows/delays, /developers/product-api/flows/drafts
**Docs index (every page):** https://staging-instasent-docs-nextjs.oscar-284.workers.dev/llms.txt
**This zone's index:** https://staging-instasent-docs-nextjs.oscar-284.workers.dev/developers/product-api/llms-full.txt
**This page:** https://staging-instasent-docs-nextjs.oscar-284.workers.dev/developers/product-api/flows/nodes/ (HTML) · https://staging-instasent-docs-nextjs.oscar-284.workers.dev/developers/product-api/flows/nodes.md (Markdown)

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](/developers/product-api/flows/templates) 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](/developers/product-api/flows/triggers-and-exits); the five kinds of wait are in [Delays and waits](/developers/product-api/flows/delays).

## How nodes chain

- **A sequential node continues on `nextStep`**, one node object. A node with no `nextStep` ends that line.
- **A branch node has `paths`** and, on most branches, a `defaultPath`. Each path is `{ "name"?, "nextStep", ...its condition }`. When a path's chain ends, the contact continues on the branch's **own** `nextStep`, shared by every path; a path with no `nextStep` goes 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`.
- **`uid` is 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.

```json
{
  "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](/developers/product-api/audience/probes#where-each-event-comes-from), segment uids from [`GET /segment`](/developers/product-api/audience/segments). 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](/developers/product-api/flows/delays#waiting-for-delivery).

- `channelType` — `string`, required
  `sms` or `rcs`.
- `abortFlowIfUnableToSend` — `boolean`, default: `true`
  When nothing can go out to this contact (no phone, no consent, a blocked number), `true` ends the run here and `false` lets it continue.
- `channel` — `object`, required
  The message.
  
  - `channelType` — `string`, required
    Repeats the node's `channelType`.
  - `compliancePolicy` — `string`
    The 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](/developers/product-api/campaigns/audience#consent-policy). **Always set it**: left out, the send also reaches contacts who refused marketing, and the draft advice flags it.
  - `options` — `object[]`, required
    Exactly one entry.
    
    - `sender` — `string`
      The id of a sender of this project for the channel. A send without one is saved but does not validate.
    - `template` — `string | object`
      The message. For SMS, the text, with [template variables](/developers/product-api/campaigns/message#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.
    - `allowUnicode` — `boolean`
      **SMS only.** Allows non-GSM characters, which lower the characters per billed part.
    - `localizations` — `object[]`
      Translations. See [Translations](#translations).
  - `fallback` — `object`
    **RCS only.** See [Fallback](#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: true` switches the fallback off without deleting it.
- `sendUnsupported` (default `true`) also sends RCS to contacts whose RCS support is unknown.

With `channel=rcs`, the [template list](/developers/product-api/flows/templates#sms-or-rcs-with-sms-fallback) 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](/developers/product-api/flows/triggers-and-exits#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](/developers/product-api/flows/delays#fixed-delay).                                                                   | `delaySeconds`.                                                                                          |

## Branches

### Fixed outcomes

The node decides which paths exist; you fill each path's `nextStep`.

- **`ACTION_BRANCH_CONTACT_CONSENT`** routes by whether the contact would pass a consent check: `channelTypes` (`sms`, `rcs`), `channelMatchMode` (`all` or `any`) and `compliancePolicy`. `paths` holds exactly one `"outcome": "allowed"` and one `"outcome": "blocked"`, and there is no `defaultPath`. 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_DELAY`** and **`ACTION_BRANCH_SMART_DELAY`**: one `waitComplete` and one `cannotSatisfy` path. See [Delays and waits](/developers/product-api/flows/delays).
- **`ACTION_BRANCH_MESSAGE_DELIVERY_WAIT`**: paths by delivery outcome. See [Waiting for delivery](/developers/product-api/flows/delays#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](/developers/product-api/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](/developers/product-api/flows/delays#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.

## What's next

- [Delays and waits](/developers/product-api/flows/delays) - Fixed, window and smart delays, and the waits for an event or a delivery.
- [Creating and editing drafts](/developers/product-api/flows/drafts) - Save the graph, validate it, and read the errors it returns.

---

This is one page of the Instasent documentation. For the complete machine-readable index of every guide and API reference, fetch https://staging-instasent-docs-nextjs.oscar-284.workers.dev/llms.txt — start there for full context.
