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

# Author a campaign journey from a natural-language prompt

> Turn a plain-English campaign brief into a validated journeyDefinition graph with POST /campaigns/journey/from-prompt, review it on the canvas, simulate it, and launch — with rollback when the draft mis-branches.

# 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](/guides/campaign-journey-builder) 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:

|                 | From-prompt                                                                                | Canvas-first                                                                                            |
| --------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| Authoring input | One paragraph brief                                                                        | Node-by-node placement, one click per step                                                              |
| Review          | One pass — read a generated graph, edit the nodes that matter                              | Incremental — you built it, so review is continuous                                                     |
| Best for        | Standard shapes: welcome series, abandoned-cart, re-engagement, win-back, drip-with-branch | Complex orchestration: multi-variant A/B inside a branch, Score Check gates on LTV, hand-tuned holdouts |
| Review surface  | The same canvas — the returned graph loads onto it                                         | The canvas itself                                                                                       |

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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/journey/from-prompt" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "intent": "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 no reply falls back to WhatsApp. Mark contacts who replied as engaged, then end the journey.",
    "available_channels": ["sms", "whatsapp", "email"],
    "tone": "friendly"
  }'
```

* `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`:

```json theme={null}
{
  "data": {
    "draft": {
      "name": "Welcome series — new signups",
      "description": "SMS welcome on signup with a WhatsApp fallback when there's no reply within 24h.",
      "definition": {
        "nodes": [
          { "id": "t1", "type": "journeyTrigger",
            "position": { "x": 400, "y": 30 },
            "data": { "label": "Signup trigger", "triggerType": "event", "config": { "eventName": "contact.created" } } },
          { "id": "s1", "type": "sendMessage",
            "position": { "x": 400, "y": 150 },
            "data": { "label": "Welcome SMS", "channel": "sms", "config": { "body": "Hi {{first_name}} — welcome aboard." } } },
          { "id": "w1", "type": "waitDelay",
            "position": { "x": 400, "y": 270 },
            "data": { "label": "Wait 1 day", "delayMinutes": 1440, "delayUnit": "days", "config": {} } },
          { "id": "c1", "type": "condition",
            "position": { "x": 400, "y": 390 },
            "data": { "label": "Replied within 24h?", "config": { "field": "journey_replied", "operator": "is_set", "value": "" } } },
          { "id": "s2", "type": "sendMessage",
            "position": { "x": 400, "y": 510 },
            "data": { "label": "WhatsApp nudge", "channel": "whatsapp", "config": { "body": "Quick hello — anything we can help with?" } } },
          { "id": "u1", "type": "updateContact",
            "position": { "x": 400, "y": 630 },
            "data": { "label": "Mark engaged", "config": { "attributes": { "journey_engaged": "true" } } } },
          { "id": "e1", "type": "journeyEnd",
            "position": { "x": 400, "y": 750 },
            "data": { "label": "End", "config": {} } }
        ],
        "edges": [
          { "id": "e_t1_s1", "source": "t1", "target": "s1" },
          { "id": "e_s1_w1", "source": "s1", "target": "w1" },
          { "id": "e_w1_c1", "source": "w1", "target": "c1" },
          { "id": "e_c1_u1", "source": "c1", "target": "u1", "sourceHandle": "yes" },
          { "id": "e_c1_s2", "source": "c1", "target": "s2", "sourceHandle": "no" },
          { "id": "e_u1_e1", "source": "u1", "target": "e1" },
          { "id": "e_s2_e1", "source": "s2", "target": "e1" }
        ],
        "holdout_pct": 10,
        "respect_smart_send_time": true
      }
    },
    "simulation": {
      "entryVolume": 1000,
      "totalProjectedSends": 1000,
      "totalProjectedCostUsd": 7.2,
      "completedVolume": 1000,
      "exitedVolume": 0,
      "channelProjections": [
        { "channel": "sms", "nodeCount": 1, "projectedSends": 1000, "projectedCostUsd": 7.5 },
        { "channel": "whatsapp", "nodeCount": 1, "projectedSends": 300, "projectedCostUsd": 1.5 }
      ],
      "warnings": []
    },
    "model": "claude-sonnet-4-6"
  },
  "meta": { "request_id": "req_example", "timestamp": "2026-09-20T12:00:00.000Z" }
}
```

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](/guides/campaign-journey-builder) 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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/journeys/simulate" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "nodes": [ ... ],
    "edges": [ ... ],
    "entryVolume": 25000,
    "assumptions": {
      "conditionPassRate": 0.6,
      "costPerSendUsd": { "sms": 0.0075, "whatsapp": 0.005 }
    },
    "scenarios": [
      { "name": "half engagement", "conditionPassRate": 0.3 },
      { "name": "2x signups", "entryVolume": 50000 }
    ]
  }'
```

A clean receipt to archive before launch (run with a sandbox key so the record is clearly a projection, not a send):

```json theme={null}
{
  "data": {
    "entryVolume": 25000,
    "totalProjectedSends": 25000,
    "totalProjectedCostUsd": 183.5,
    "completedVolume": 24100,
    "exitedVolume": 900,
    "channelProjections": [
      { "channel": "sms", "nodeCount": 1, "projectedSends": 25000, "projectedCostUsd": 187.5 },
      { "channel": "whatsapp", "nodeCount": 1, "projectedSends": 8000, "projectedCostUsd": 40.0 }
    ],
    "terminalOutcomes": { "completed": 24100, "exited": 900, "unresolved": 0 },
    "warnings": []
  },
  "meta": { "request_id": "req_sim_1", "timestamp": "2026-09-20T12:05:00.000Z", "test_mode": true }
}
```

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](/guides/campaign-journey-builder).

## 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. **Launch** — `POST /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](/troubleshooting/campaign-journey-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](/concepts/rate-limit-cooldown-collector) 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-brief`](/api-reference/endpoints/campaigns) drafts per-channel **copy** (subject, body, CTA) for a **single blast** — no nodes/edges graph.
* [`POST /agents/from-prompt`](/guides/agent-from-prompt) drafts 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)](/guides/campaign-journey-builder) — the node-by-node canvas loop, the validator gates, and the full simulator reference.
* [Author an AI Agent from a Prompt](/guides/agent-from-prompt) — the sibling from-prompt builder for conversational agents.
* [Send a campaign end-to-end](/guides/campaign-end-to-end) — blast/drip lifecycle and launch mechanics.
* [Troubleshooting: campaign and journey enrollment errors](/troubleshooting/campaign-journey-errors) — the refusal codes a bad graph returns.
* [Campaigns API reference](/api-reference/endpoints/campaigns) — request/response shapes for every endpoint cited here.
