Skip to main content

Author a campaign journey from a natural-language prompt

Writing a journey by hand means opening the canvas, dragging every node, wiring every edge, and naming every branch handle — fine for a complex graph, overkill for a welcome series you can describe in two sentences. The from-prompt builder takes that plain-English brief, generates a structured journeyDefinition (nodes + edges) that passes the same save-time validator the canvas uses, and hands it back for review. Nothing is persisted until you commit it, and nothing is sent until you launch. This guide walks the full path: decide when from-prompt beats the canvas, write a prompt the generator can parse deterministically, call POST /campaigns/journey/from-prompt, review the returned graph on the canvas, dry-run it in the simulator, launch — and roll back when the simulation flags a mis-branch. Build, simulate, and launch a campaign journey covers the canvas-only build loop in depth; this page covers the NL-first loop that hands off to the same canvas.

1. When from-prompt beats the canvas

The two entry points produce the same artifact — a variables.journeyDefinition graph on the campaign — so the choice is authoring speed, not capability: A brief you can read aloud in one breath — audience, trigger, channels, waits, exit — generates faster than it draws. If your mental model already has three nested conditions and a holdout carve-out, start on the canvas instead; the generator describes linear and single-branch shapes better than it invents elaborate ones.

2. Author the prompt

The generator reads one intent string. Prompts that generate a clean graph on the first call share a shape:
  1. Audience + trigger — who enters, and what starts them (“new signups when contact.created fires”, “contacts who abandon a cart”).
  2. Steps in order — the channels and content beats in sequence (“welcome by SMS, then a day later an email with tips”).
  3. Waits — explicit durations between beats (“wait 1 day”, “after 3 days”).
  4. Branches — plain conditional language, one fork at a time (“if they don’t reply within 24 hours, fall back to WhatsApp”).
  5. Exit — how the journey ends (“then end”, “escalate to a human if they ask for one”).
A strong welcome-series prompt:
Welcome new signups when the contact.created event fires: send an SMS greeting, wait 1 day, wait up to 24 hours for a reply, and if there’s no reply fall back to a WhatsApp nudge. Mark contacts who replied as engaged, then end the journey.
An abandoned-cart prompt:
When a cart.abandoned event fires for a contact, send them an SMS reminder after 30 minutes. If they haven’t completed the purchase within 6 hours, send a follow-up email. End the journey after the email.
Phrasing that stays regex-free and channel-pure — the generator maps prose onto node types; it does not need you to pre-specify handles or encodings:
  • Say “if they didn’t reply” rather than describing a conditionField / conditionOperator pair — the generator picks the node type and the yes/no handles; you review the predicate on the canvas.
  • Say “SMS” or “WhatsApp”, never a carrier or a phone number. The graph carries channel semantics only — outbound routing is owned by the platform’s send path (a voiceCall node carries a spoken TTS script or an agent id, never a carrier/route override).
  • One fork per sentence. “If engaged do X, else if high-value do Y, else do Z” is three branches describing a graph the canvas builds better than the prompt parses.
Optional hints available_channels and tone further constrain the output — see the request below.

3. POST /campaigns/journey/from-prompt

Send the brief. The response is a draft — validated, but not persisted:
  • intent (required) — 8–4000 chars; one journey described in prose.
  • available_channels (optional) — restrict generated sendMessage nodes to channels you’ve actually connected, so the draft never contains an RCS node when RCS isn’t provisioned. Omitted → the standard set (sms, whatsapp, email, push, rcs).
  • tone (optional) — free-form hint (“friendly”, “formal”, “urgent”) the generator applies to message bodies.
The response envelope carries data.draft + data.simulation:
Three things in the response worth reading before you touch the canvas:
  • draft.definition — the validated graph. holdout_pct: 10 and respect_smart_send_time: true are inserted by the endpoint (not the model) so every AI-authored journey carries a measurable-lift control group and per-contact send-time arbitration by default; adjust either on the canvas’s Journey Settings panel before saving.
  • draft.definition.nodes[].position — canvas layout coordinates the graph arrives with (single-column, top-down). The canvas renders them unchanged; you rearrange after load.
  • simulation — a read-only projection over a default 1,000-contact cohort: per-channel projected sends and cost, terminal outcomes, and structural warnings. null when the projection couldn’t run — the draft still loads. Re-run it with your real cohort and costs once the graph is on the canvas (next section).
If the response is 422 AI_JOURNEY_INVALID or 422 AI_PARSE_FAILED, the model drifted — the endpoint ran the same Zod + cross-node validation the save path would and rejected the graph instead of handing you a broken canvas. Retry with a clarified intent; if it recurs, narrow the prompt to the five-part shape above. A 503 means no LLM provider is configured for the tenant — build on the canvas directly.

4. Review the generated graph on the canvas

Open Campaigns → Journey in the dashboard. The empty canvas offers the Describe with AI entry point next to the template catalog — submitting your brief there calls the same endpoint, shows the preview card (projected sends + cost), and loads the graph onto the canvas only after you confirm Load onto canvas. Loading over the API instead? The same draft.definition payload commits via the wizard’s graph paste or PATCH /campaigns/:id with variables.journeyDefinition. Once on the canvas, the graph is a normal journey draft — every node opens its config panel, every edge carries its handle. The review pass that matters:
  1. Read each node label + body once. The generated message copy is a starting point — tighten it in the config panel before anyone sees it.
  2. Confirm the trigger. The generator defaults to triggerType: "all" for broad audience descriptions because it is never given a real segment id — scope the actual audience in the Audience panel (or re-point the trigger at the event the brief named).
  3. Confirm branch handles. A condition or scoreCheck node must carry outgoing yes AND no edges; a waitForEvent its event and timeout handles. The same validator Build, simulate, and launch a campaign journey documents runs at save — the canvas blocks an unwired branch, and the server re-checks the same rules so a draft can’t sneak through the API path.
  4. Set available_channels you actually have. A tenant without WhatsApp provisioned gets a graph that saves fine but fails sends at runtime — pass the hint, or swap the node’s channel on the canvas.

5. Simulate before launch

The endpoint’s inline simulation preview ran on a default 1,000-contact cohort with platform default rates — a shape check, not your answer. Before launch, run the simulator against the actual graph and your real cohort:
A clean receipt to archive before launch (run with a sandbox key so the record is clearly a projection, not a send):
Healthy: arrivals taper through branches, no dead steps in warnings, unresolved: 0, projected spend inside your campaign’s credit_cap_usd_cents. The sandbox variables (a dv_test_sk_* API key, or X-Test-Mode: true on a Clerk session) keep the projection clearly separated from live sends — nothing is ever enrolled by this endpoint regardless, but test-mode keeps your request logs honest. Full simulator semantics — what assumptions can override, what scenarios compare, what warnings flag — are in the canvas guide’s simulator section.

6. Launch and roll back

Committing and launching are two separate acts, and the draft stays a draft until both happen:
  1. Commit — the wizard saves the graph onto the campaign as variables.journeyDefinition; the draft campaign exists but isn’t running.
  2. LaunchPOST /campaigns/:id/send flips the campaign to running and the trigger starts enrolling contacts one at a time.
When the simulator (or the first live cohort) flags a mis-branch — contacts routing no when the canvas reads yes, a fall-through edge the brief didn’t intend — rollback is cheap because the journey version is the graph itself:
  • Before launch — edit the canvas, re-save, re-simulate. Nothing is live.
  • After launch — pause the campaign (POST /campaigns/:id/pause; resume with POST /campaigns/:id/resume), fix the graph on the canvas, save as a new version, relaunch. The prior version stays attached to the campaign’s history, so relapsing to it is a re-save of the older graph, not a rebuild. Paused journeys keep their enrollment state; resuming continues where they stopped.
  • A bad branch on a live journey — the executor fails closed: an unwired yes/no handle means half your contacts have nowhere to go. If simulation or node analytics show a branch bleeding to nowhere, pause first, then fix.
The launch-time validator refuses a structurally broken graph (JOURNEY_DEFINITION_INVALID, JOURNEY_TRIGGER_MISSING, JOURNEY_TRIGGER_DISCONNECTED) — the same codes the canvas blocks on, returned server-side for SDK/API callers. Troubleshooting: campaign and journey enrollment errors maps each code to its fix.

7. Limits — where from-prompt hands back to the canvas

The generator emits a curated subset of the journey palette: trigger, sendMessage, wait, condition, scoreCheck, abSplit, channelPreference / smartChannel, voiceCall, ussdPush, aiAgent, updateContact, webhook, humanHandoff, and the terminal nodes. The low-level lifecycle primitives (addTag / removeFromList / incrementAttribute) stay canvas- or API-authored — a prompt describing a journey in prose reaches for higher-level steps, and updateContact covers the attribute-write intent. Complex Condition or Score Check logic — nested operators, a threshold tuned to a specific percentile, regex predicates — is a canvas refinement job: generate the linear spine from the prompt, then open the node and rebuild the predicate in the config panel, or replace the node entirely. Rate limits: the endpoint sits on the tighter agent-invoke bucket shared with every LLM-backed route — see the rate-limit collector concept for the per-bucket caps and Retry-After contract. Budget your retries and don’t loop regeneration while tuning wording; iterate on the canvas once the shape is right, and regenerate only when the shape itself is wrong. The same idempotency rules as every mutating endpoint apply — pass Idempotency-Key on retries. Two adjacent generators are deliberately distinct:

See also