SEND_MESSAGE node sends an approved WhatsApp template. Templates have
positional placeholders ({{1}}, {{2}}, …). You fill each one by setting
templateBindings on the node: a map from a placeholder to the path of
the value it should carry:
The catalog is the source of truth. The variable catalog (
GET /api/v1/journeys/{journeyId}/message-variables, the
journeys_message_variables MCP tool, or the visual builder’s picker) is
authoritative for a specific journey. It already accounts for the journey’s
trigger, segment, and schemas. If this page and the catalog ever disagree,
trust the catalog.Where the values come from
Each namespace is supplied by a different actor:
Namespace notes:
person.*re-resolves at send time from the CDP person the enrollment named, and is offered for every trigger, including event-triggered journeys. The phone or email lookup is only a fallback, used for contacts enrolled without a CDP person, and that fallback needs exactly one match: if several people share the phone or email, the value resolves blank. Full resolution order below.engagement.context.*is validated against the initiative’s context schema and only exists when you enroll the participants yourself (CSV, manual add, API). Event- and segment-triggered enrollment never populates it.engagement.workflowState.*carries what the trigger delivered (the event’s properties, the segment’s output columns, or an optional payload on manual enrollment), frozen at enrollment._eventis a reserved key.engagement.extracted.*requires a published extraction schema and an upstream conversation block that has already run.
plan) is referenced as
person.plan, not attributes.plan.
What resolves, per trigger
For the first message of a journey:person.* values are read live at send time, not frozen at enrollment, so
they behave the same for every trigger. Which CDP person they are read from is
decided in this order:
- The person the enrollment named. If you enrolled the contact by
personExternalId, which an event-triggered journey always does, that identity is used directly. It is authoritative: if that person has since been deleted or removed,person.*resolves blank rather than falling back. - Otherwise, a lookup by phone or email. For contacts enrolled without a CDP person (manual add, CSV import), Boom matches the contact back to a person. This needs exactly one match: zero matches, or several people sharing that phone number, resolve blank.
person.* on path 2. If your events
carry personExternalId, duplicates don’t affect you.
engagement.extracted.*, engagement.nodeOutputs.*, and engagement.documents
are produced during the run, so they resolve later in the journey for every
trigger type, once the step that produces them has run.
Worked examples
Event-triggered journey: the event’s properties are the participant data. An event likeloan_approved with a plan property binds as:
person.<key>:
Discover the valid variables
Rather than guess, ask the API (or MCP) for the exact set a given journey can bind: the same list the visual builder shows:path.
Copy a path straight into templateBindings. For event-triggered journeys
the response includes a notes field explaining which groups are unavailable
and why.
Validation
When you save or validate a journey, a binding whose path can’t resolve (for exampleattributes.plan or customer.attributes.plan) is reported as an
error and blocks publishing, so a broken binding never ships silently. Fix it by
switching to a path from the catalog above (custom attributes → person.<key>).