> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useboom.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Journeys

> The versioned workflow behind an initiative. Build it, validate it, and publish it, all over the API.

A journey is the step-by-step workflow a person moves through inside an initiative: a message, a wait, an AI-led conversation, a branch, all the way to an exit. It is fully authorable over the API and MCP, not read-only. You build a draft node by node, wire the connections, set how people enter, then validate and publish.

## Journey, initiative, run

Every journey belongs to exactly one initiative. A new initiative arrives without one, so building the first draft is your first step after creating it, and an initiative cannot launch until that draft exists and validates. From there the initiative can carry several journey versions over its life:

```
Initiative "Renewal reminder"
  Journey v1  STOPPED     (superseded by v2; its runs ran to completion)
  Journey v2  PUBLISHED   (live, one run per enrolled person)
  Journey v3  DRAFT       (being edited)
```

Each enrollment creates its own run against the version that was live at the moment the person entered. `journeys_list` and `journeys_get` return the read-only summary of a journey (its trigger and ordered steps, in plain language). `journeys_get_definition` returns the full editable graph, the shape you build with and save back.

## Node kinds

Discover these from `journeys_authoring_catalog` rather than hardcoding them: it returns every kind's inputs, its output handles (the signals it can emit), and the connection rules, scoped to what your organization actually has enabled.

| Node | What it does | Notable rule |
| - | - | - |
| `ENTRY` | Where people start: manual, segment, CDP event, or an inbound message, with an optional frequency cap | Exactly one per journey |
| `SEND_MESSAGE` | Sends a WhatsApp message from a chosen channel, in one of two content modes: an approved template (`mode: "template"`, and what you get when you omit `mode`), or text you write yourself (`mode: "free_text"`) | In template mode the template must belong to the same WhatsApp account as the channel, or publishing is blocked, and the node emits `SENT` plus `SKIPPED` (your organization's Smart Sending cap refused the send). Wiring `SKIPPED` is optional: leave it and the run simply ends, or wire it to retry later or try another channel. Free text emits **two** handles, `SENT` and `WINDOW_CLOSED`, and both must be wired: Meta accepts free-form content only within 24 hours of the person's last message, so wire `WINDOW_CLOSED` to an approved template as the fallback. Free text needs `body` plus a `bindings` entry per `{{placeholder}}` instead of `templateId`. Set `delivery: "draft"` (template mode only) to hold each send for approval in the campaign's **Drafts** tab (see [drafts](/drafts)); it adds a third handle, `REJECTED`, which can stay unconnected to end the run. An approved draft the cap refuses at send time still takes `SKIPPED`. If drafts are turned off for your organization, publishing a drafted send is refused with `drafts_disabled` |
| `SEND_WHATSAPP_TEXT` | The free-text WhatsApp send under its older name | Deprecated, and still executable — journeys already built with it keep running and can still be read back. Build new ones as a `SEND_MESSAGE` with `mode: "free_text"`, which is the same send |
| `SEND_SMS` | Sends a plain-text SMS from a chosen number | Emits `SENT` plus the optional `SKIPPED` (Smart Sending's cap refused the send), like a template `SEND_MESSAGE`. Every `{{placeholder}}` in `body` needs a matching entry in `bindings`, or publishing is blocked — an unbound one would be sent literally, braces and all. Behind a feature flag your organization may not have yet |
| `SEND_INSTAGRAM` | Sends a free-text Instagram message from a chosen native account | No template concept, so every message is author-written prose. Emits **two** handles, `SENT` and `WINDOW_CLOSED`, and both must be wired: Meta accepts free-form content only within 24 hours of the person's last message. Instagram has no template fallback, so `WINDOW_CLOSED` usually leads to an `EXIT` rather than another send. In a run an `INSTAGRAM_COMMENT` trigger started, a shut window sends one private reply to that comment instead (Meta allows one per comment, within 7 days). Behind a feature flag your organization may not have yet |
| `SEND_EMAIL` | Sends a PUBLISHED email template to the person's email address | Emits `SENT` plus the optional `SKIPPED` (Smart Sending's cap refused the send). The next step cannot be `WAIT_FOR_REPLY`, `MANAGE_CONVERSATION` or `CONVERSATION_BLOCK`, since email has no reply of its own. Publishing needs the template PUBLISHED and a From address that resolves (a verified domain with a default sender, or a valid `fromSenderIdOverride`); `journeys_email_templates` shows both. Also takes `delivery: "draft"` ([drafts](/drafts)), same `REJECTED` handle; `REJECTED` and `SKIPPED` follow the same successor rule as `SENT` |
| `SEND_MESSENGER` | Sends a free-text Facebook Messenger message from a chosen Page | Same rules as `SEND_INSTAGRAM`: author-written prose, **two** handles, `SENT` and `WINDOW_CLOSED`, both wired, because Meta accepts it only within 24 hours of the person's last message. In a run a `FACEBOOK_COMMENT` trigger started, a shut window sends one private reply to that Page comment instead, as for Instagram. Up to 2000 characters. Behind a feature flag your organization may not have yet |
| `WAIT_FOR_REPLY` | Waits for the person to reply | Emits `REPLIED` or `TIMEOUT`. Pair it with `MANAGE_CONVERSATION` |
| `MANAGE_CONVERSATION` | Runs the AI-led conversation, or hands it to a human (`mode: ESCALATE`) | Emits `CLOSED` or `STALE`. An optional `goal` gives the agent instructions for this step only, on top of the initiative objective. An optional inactivity timeout closes the conversation after a window of silence, unless the agent has handed it to a human |
| `CONVERSATION_BLOCK` | Legacy combined wait-and-converse step | Kept so journeys published before the split keep running. Build new journeys with `WAIT_FOR_REPLY` + `MANAGE_CONVERSATION` instead |
| `DISPATCH_EVENT` | Records a CDP event for the person | Lets one journey enroll people into another: the dispatched event can be a different journey's trigger |
| `DELAY` | Pauses the run for a duration, until a date, or until a weekday window | A pure wait. It does not race an incoming reply |
| `DECISION` | Two-way branch, combined by AND/OR, over workflow data, a reserved event, or a live CDP attribute | Both `YES` and `NO` must be wired before publishing |
| `CASE` | Switches on a single person attribute, up to 10 branches | Every branch handle, plus the default handle, must be wired |
| `HTTP_REQUEST` | Calls an external endpoint, optionally with a stored credential | Emits `SUCCESS` or `FAILED`. The url, headers and body interpolate `{{variable}}` tokens from a different set than a message binding uses — see the node's own inputs in `journeys_authoring_catalog`. Behind a feature flag your organization may not have yet |
| `ROUTE_TO_INITIATIVE` | Ends the journey for the person and enrolls them into a different initiative, moving their live conversation onto it | Terminal, so it cannot have an outgoing connection. The target must be another live initiative in the same organization, and it should open by sending: the message that caused the routing belongs to the old conversation, so the target cannot reply to it |
| `EXIT` | Ends the journey for the person | At least one end point is required — an `EXIT` or a `ROUTE_TO_INITIATIVE` |

<Note>
  `SEND_EMAIL` sends a PUBLISHED email template and emits `SENT`, plus the optional `SKIPPED` when Smart Sending's cap refuses the send. It never emits a reply signal, so the step after it cannot wait for a reply or run a conversation (see Troubleshooting). Pick its template and check sender readiness with `journeys_email_templates`. Email has no `channelId`: it sends from the organization's verified domain. Author the templates themselves with the [email template tools](/email-templates).
</Note>

## Build a journey

<Steps>
  <Step title="Discover what you can build">
    Call `journeys_authoring_catalog` for the node kinds, and the reference catalogs for the ids and paths a node needs: `journeys_message_channels` for any send node's `channelId` (WhatsApp, SMS, Instagram and Messenger — each row carries `type` and the `sendNodeKind` that takes it), `journeys_message_templates` for a `SEND_MESSAGE` node's approved template, `journeys_email_templates` for a `SEND_EMAIL` node's published email template, sender and email readiness (email has no `channelId`), `journeys_event_catalog` for event names, `journeys_condition_catalog` for `DECISION`/`CASE` attribute paths, and `journeys_message_variables` for what a template placeholder can bind to.
  </Step>

  <Step title="Start a draft">
    `journeys_create_draft` with an `initiativeId` and a definition (name, nodes, edges). Omit node positions and the server lays the graph out for you. To keep editing an initiative's existing draft instead, load it with `journeys_get_definition`.
  </Step>

  <Step title="Add and wire nodes">
    Shape the graph with `journeys_add_node`, `journeys_update_node`, and `journeys_delete_node`, then connect them with `journeys_connect_nodes`, naming the source node's output handle (`SENT`, `REPLIED`, `YES`, `case:<id>`, and so on). A handle wires to at most one node. `journeys_disconnect_nodes` removes an edge.

    `journeys_update_node` merges the `inputs` you send into the ones the node already holds, and checks the *merged* node against the same per-kind rules `journeys_add_node` applies. So a patch that is fine on its own is refused when it contradicts what the node already stores, such as sending a `templateId` to a node whose `mode` is `free_text`. A half-configured node is never refused for being incomplete — that is what `journeys_validate` is for.
  </Step>

  <Step title="Set the trigger">
    `journeys_set_trigger` configures the ENTRY node: `manual`, `segment` (needs a `segmentId`), `cdp_event` (needs an `eventName`), or `inbound` (needs an `inboundAction` and `inboundChannelIds`), with an optional frequency cap.

    <Warning>
      A segment trigger needs the segment's internal id, and the public API
      identifies segments by `slug` instead, so you cannot wire one from the API
      or MCP today. Passing a slug fails with a not-found error. Set a segment
      trigger in the Boom app. Manual and event triggers work fully from here.
    </Warning>
  </Step>

  <Step title="Validate">
    `journeys_validate` dry-runs the publish checks against a stored draft or an arbitrary definition, without saving anything. It returns whether the graph is valid and the full list of issues, errors block publishing, warnings are advisory.
  </Step>

  <Step title="Publish">
    `journeys_publish` with `confirm: true`. This is the one action here that starts real outreach.
  </Step>
</Steps>

## Publishing and versioning

Publishing is deliberately guarded:

* It validates first. Any error-severity issue blocks the publish, the same checks `journeys_validate` runs.
* Warning-severity issues do not block it, and come back on the response as `warnings`, an array in the same shape `journeys_validate` returns. It is empty on a clean publish. Read it: some warnings cover conditions the validator cannot decide on its own, such as an inbound-triggered journey whose first step is a free-text message on a channel whose AI agent may also answer, which would send the person two replies.
* You do not have to publish to see them. Calling `journeys_publish` **without** `confirm: true` publishes nothing and refuses, and that refusal lists the warnings the draft carries — in the error message, and in `issues` on the error body. So the safe two-step call, preview then confirm, shows you what a clean publish would not. `journeys_validate` returns the same warnings if you would rather ask before you are ready to publish at all.
* It requires an explicit `confirm: true`. There is no accidental publish, because publishing enrolls real people and sends them real messages.
* It is atomic. The previous PUBLISHED version is retired in the same operation that promotes the new one, never a moment where two versions are both live.
* People already mid-journey are unaffected. Each run is pinned to the version it enrolled under, so publishing a new version never changes someone's path partway through.

Published journeys are frozen: there is no in-place edit. To change anything, even a small fix, fork it first with `journeys_create_draft_from_published`, which copies the live version into a new editable draft while the original keeps running untouched. Edit the fork, validate it, and publish it, which then supersedes the version you forked from.

A journey reaches STOPPED two different ways, and they do not mean the same thing. Publishing a replacement supersedes the old version, which keeps running out the people already on it. Stopping it retires it outright. Both show `status: STOPPED`; only the second sets `stoppedAt`.

## Stopping a journey

Publishing a replacement is not the only way to take a campaign down. `journeys_stop` retires the live version without putting anything in its place. It takes either a journey id or the initiative id, and needs `confirm: true`.

* **Nobody else enrolls.** Both the trigger and the initiative's live-journey pointer are closed, so a segment backfill still in flight stops producing enrollments too.
* **Anyone enrolled but not yet messaged is dropped** at their send instead of receiving it. That includes people queued behind a large batch still draining.
* **Conversations already under way continue normally.** Stopping is not cancelling — it closes the front door and leaves the people inside alone.
* **Sends waiting for approval are canceled.** Drafts from the stopped version move to Canceled and those runs end, so the person can be enrolled again.

Stopping leaves the initiative with no PUBLISHED journey at all, so it does not undo itself and launching the initiative will not revive it. To run the campaign again, fork the version you want with `journeys_create_draft_from_published` and publish it.

## Triggers

A journey enters people one of four ways, set on its ENTRY node:

* **Manual**: an operator or an API call adds people directly.
* **Segment**: anyone who enters the segment enrolls. Pass `includeExisting: true` on publish to also backfill current members once. Backfill takes whoever is a member at that moment, so a segment that has never been evaluated has no members and backfills nobody, with no error to tell you.
* **CDP event**: a person enrolls the moment a matching event arrives and resolves to a known person.
* **Inbound**: a person enrolls the moment they message one of the journey's channel instances (WhatsApp, SMS, or Instagram), rather than something reaching out to them first. Set with `journeys_set_trigger`'s `inboundAction`, `inboundChannelIds`, and an optional `inboundKeyword` (matched by `inboundMatchMode`: `EXACT`, `CONTAINS`, or `ALL_WORDS`). Leaving the keyword empty makes the trigger a catch-all for every inbound message on those channels. Several journeys may listen on the same (organization, channel, action) — a keyword tells them apart, and resolution picks one winner per message.

An optional frequency cap (`maxEnrollments` plus `enrollmentWindow`, set together or not at all) limits how often the same person can re-enter within a rolling window, useful for a journey that can otherwise re-trigger on repeat events or segment membership. The cap is per person, never a campaign-wide total: `maxEnrollments: 1` stops one person entering twice, it does not stop the journey enrolling new people indefinitely. A journey using **several runs at once** (below) cannot also carry a cap — publish refuses the combination, because that journey already refuses a repeat of the same value and a per-person cap on top would drop the later runs.

## Several runs at once

By default a person holds one run of a journey at a time, and a second triggering event while that run is live is dropped. That is right for a conversation and wrong for a transaction: someone who takes two credits in a day needs two payment links, not one.

A journey triggered by a **CDP event** can set `parallelRunsBy` on its ENTRY node to the name of a property on that event — `paymentLinkId`, `orderId` — and then each distinct value of that property starts its own run for the same person, carrying its own event's data into its messages.

In the builder, open the trigger, pick the event and type the property under **One run per**. Leave it blank for the default one run per person. Over the API or MCP, pass `parallelRunsBy` to `journeys_set_trigger`. Several runs at once is enabled per organization: if the builder doesn't show **One run per**, ask your Boom contact.

The property has to be a top-level field of the event's `properties` (`payment_pkey`, not `payment.pkey` inside a nested object), and event names use letters, numbers and underscores only.

What that changes:

* **A repeat of the same value never starts a second run.** A value is good for one run ever, whatever happened to it, so a retried or replayed event cannot restart a sequence that already finished. The flip side: a run that failed cannot be re-run for that same value.
* **An event missing the property is refused**, not enrolled. Enrolling it would send a message with blank placeholders.
* **Only the triggering event can start a run.** Adding a person to the journey by hand, or through the API, is refused for the same reason — there is no event to take the property from. Don't point a segment at a journey like this either.
* **The journey can only send.** No Wait for reply, no Handle conversation, no legacy business-hours delay on a send. A customer's reply arrives on the person, not on one run, so a single reply would release every run at once — one "ok gracias" about the first credit would silently drop the pending reminders for the others.
* **A stop event stops only its own run.** The stop event has to carry the same property with the same value. That is the point — one link being paid must not stop the reminders for the person's other links.
* **A stop event that does not carry the property stops nothing.** This looks like a bug and is not: the alternative is stopping every run the person has.

Because a stop event and a triggering event are processed independently, a stop event can arrive before the run it would have stopped exists, in which case the run still starts. Put a **DECISION** before each send in a reminder loop — "is this link still pending?" — rather than relying on the stop event landing first.

Everything about a journey that does not set `parallelRunsBy` is unchanged, including the one-run-at-a-time rule.

## Troubleshooting

**A template placeholder sends blank instead of erroring.** It is usually bound to a path that does not resolve for this journey's trigger. Call `journeys_message_variables` for the exact set of paths this journey can use. A custom person attribute binds as `person.<key>`, not `attributes.<key>`, that second form is the syntax for `DECISION`/`CASE` conditions, not message bindings.

**A `{{variable}}` in an HTTP request sends blank instead of erroring.** Same symptom, different answer: `journeys_message_variables` describes what a *message* can bind and does not list the HTTP-only groups, so it will not tell you why an HTTP body came out empty. The roots an HTTP node resolves are documented on the node's own `url`, `headers` and `body` inputs in `journeys_authoring_catalog`, and validation now reports an unresolvable token as `UNRESOLVABLE_HTTP_TOKEN` when you validate or publish.

It is a **warning, not an error**, so it never blocks a publish that would have worked: the built-in customer fields are guidance rather than a closed list, and a request may legitimately reference a column the catalog does not know about. Read the warnings on every publish rather than only the errors. The most common cause is a near-miss on a built-in field — `customer.phone` instead of `customer.phoneNumber` — which substitutes as an empty string and sends a request that looks successful.

**Publish fails because a conversational node follows an email send.** An email-send step has no reply lifecycle: only a delay, an exit, or another non-conversational node may come after it. Route any reply-driven path around it instead of through it.

**Publish fails with validation errors you did not expect.** `journeys_publish` always validates before publishing and refuses on any error-severity issue, it does not publish "mostly working" journeys. Run `journeys_validate` while you iterate so you see the same issues before you attempt to go live.

**A time-limited campaign keeps enrolling people after its date has passed.** Nothing stops it on its own: a published journey stays live until something takes it down, a frequency cap only limits one person's re-entry, and a `DELAY` set to a date that has already gone by resolves immediately rather than holding people back. Take it down with `journeys_stop`.

## Related

<CardGroup cols={2}>
  <Card title="Use MCP" icon="plug" href="/use-mcp">
    Connect an AI tool to author and run journeys conversationally.
  </Card>

  <Card title="Template variables" icon="message-square" href="/template-variables">
    The full variable catalog a SEND\_MESSAGE binding can reference, and what resolves per trigger.
  </Card>

  <Card title="Events" icon="bolt" href="/events">
    Record the CDP events that trigger a journey or feed a DECISION condition.
  </Card>

  <Card title="One uniform surface" icon="layers" href="/one-surface">
    Why every journey tool here has a matching REST endpoint.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.