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 structuredjourneyDefinition (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 — avariables.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 oneintent string. Prompts that generate a clean graph on the first call share a shape:
- Audience + trigger — who enters, and what starts them (“new signups when
contact.createdfires”, “contacts who abandon a cart”). - Steps in order — the channels and content beats in sequence (“welcome by SMS, then a day later an email with tips”).
- Waits — explicit durations between beats (“wait 1 day”, “after 3 days”).
- Branches — plain conditional language, one fork at a time (“if they don’t reply within 24 hours, fall back to WhatsApp”).
- Exit — how the journey ends (“then end”, “escalate to a human if they ask for one”).
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/conditionOperatorpair — the generator picks the node type and theyes/nohandles; 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
voiceCallnode 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.
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 generatedsendMessagenodes 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.
data.draft + data.simulation:
draft.definition— the validated graph.holdout_pct: 10andrespect_smart_send_time: trueare 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.nullwhen 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).
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 samedraft.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:
- Read each node label + body once. The generated message copy is a starting point — tighten it in the config panel before anyone sees it.
- 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). - Confirm branch handles. A
conditionorscoreChecknode must carry outgoingyesANDnoedges; awaitForEventitseventandtimeouthandles. 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. - Set
available_channelsyou 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 inlinesimulation 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:
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:- Commit — the wizard saves the graph onto the campaign as
variables.journeyDefinition; the draft campaign exists but isn’t running. - Launch —
POST /campaigns/:id/sendflips the campaign torunningand the trigger starts enrolling contacts one at a time.
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 withPOST /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/nohandle means half your contacts have nowhere to go. If simulation or node analytics show a branch bleeding to nowhere, pause first, then fix.
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, andupdateContact 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:
POST /campaigns/from-briefdrafts per-channel copy (subject, body, CTA) for a single blast — no nodes/edges graph.POST /agents/from-promptdrafts an AI conversational agent (one bot with a system prompt + tools), not a multi-step journey.
See also
- Build, simulate, and launch a campaign journey (canvas) — the node-by-node canvas loop, the validator gates, and the full simulator reference.
- Author an AI Agent from a Prompt — the sibling from-prompt builder for conversational agents.
- Send a campaign end-to-end — blast/drip lifecycle and launch mechanics.
- Troubleshooting: campaign and journey enrollment errors — the refusal codes a bad graph returns.
- Campaigns API reference — request/response shapes for every endpoint cited here.