# Journey documents

A journey is one document (JSON, or the same document as YAML), saved as an immutable **version**:
`draft` → `active` (one per journey; each template's version is pinned at activation) → `draining` (keeps its
enrolments, takes no new ones) → `stopped`. Saving the same document again makes no new version. The schema is
`apps/api/src/modules/journey-document.ts`; a worked example is `examples/welcome/journey.yaml`.

```yaml
key: welcome                     # 2-63 lowercase letters, digits and dashes; must match the URL / tool key
name: Welcome series
brand: main                      # a brand key
subscriptionType: news           # one of the brand's subscription types: the consent every send checks
entry:
  on: consent_granted            # enter when a person gains consent for brand + subscriptionType
  # on: { event: signed_up }     # or: enter on a tracked event (they still need consent to be sent to)
reentry: never                   # each person enters once, ever (the only option today)
goal: { event: activated }       # optional: the event that counts as success; exits them (exit: true)
exitWhen:                        # optional, up to 20: leave as soon as one is true
  - { event: unsubscribed_everywhere }
  - { property: plan, equals: enterprise }
start: welcome                   # the first step's key
steps:
  welcome:   { type: send, template: welcome, next: wait-2d }
  wait-2d:   { type: wait, days: 2, next: clicked }          # days, hours and/or minutes; at most 365 days
  clicked:
    type: branch
    if: { clicked: welcome }     # a human click on the email sent at send step `welcome`
    then: next-steps
    else: nudge
  next-steps: { type: send, template: next-steps, next: done }
  nudge:      { type: send, template: nudge, next: done }
  done:       { type: exit }
```

## Steps

Step keys: lowercase letters, digits, dashes and underscores. Every step except `exit` names where to go next.

| Type | Fields | What it does |
|---|---|---|
| `send` | `template`, `next` | Renders the template for the contact and sends it, if the eligibility check allows (consent, suppression, frequency cap, sandbox allowlist, verified sending domain, the reputation guard). A refusal is a **skip** with a reason; the enrolment still moves on. Over the sending domain's warm-up cap the step is **deferred** (`warm_up`): the enrolment waits at the step until the next day, then sends. |
| `wait` | `days` / `hours` / `minutes`, `next` | Waits. Sandbox accounts divide waits by their time scale. |
| `branch` | `if`, `then`, `else` | `if` is one of `{ clicked: <send step> }`, `{ event: <name>, within: { days: 3 } }`, `{ property: <name>, equals: <value> }`. |
| `split` | `percent`, `a`, `b` | Sends `percent`% of people to `a`, the rest to `b`. |
| `update` | `properties`, `next` | Sets contact properties. |
| `webhook` | `url` (https), `next` | POSTs the enrolment to your URL. |
| `exit` | | Ends the enrolment. |

## Validation

`journey_validate` (and every save) checks the schema, the graph and the references. Each error names the `step`
(when there is one), the `field`, the `rule` and a `fix`:

| Rule | Meaning |
|---|---|
| `invalid_field`, `unknown_step_type` | The document does not match the schema (unknown fields are errors). |
| `key_mismatch` | The document's `key` differs from the key you saved it under. |
| `unknown_step` | `start`, `next`, `then`, `else`, `a` or `b` points at a step that does not exist. |
| `unreachable_step` | No path from `start` reaches the step. |
| `cycle_without_wait` | A loop with no wait in it would run forever. |
| `invalid_wait`, `wait_too_long` | A wait with no duration, or over 365 days. |
| `clicked_not_a_send` | A `clicked` branch names a step that is not a send. |
| `webhook_not_https` | Webhook URLs must be https. |
| `unknown_brand`, `unknown_subscription_type`, `unknown_template` | A reference that does not exist yet: create it first. |
| `template_brand_differs` (warning) | The template was written for another brand. |

## Before it goes live

1. `journey_plan`: the step diff against the active version, the template versions activation would pin, and how
   many people are in progress on the active version (they finish on it; nobody is migrated).
2. `journey_simulate`: the path a real contact (`contactId`) or sample data would take, with `events` (`afterDays`)
   and `clicked` send steps. Nothing is sent.
3. `journey_request_activation`: an agent key gets `{ approvalRequired, approvalId, consoleUrl }` and an owner
   approves; an owner key activates. The previous active version drains.
4. Afterwards: `journey_funnel` per step, `enrolment_trace` for one person (why they did or didn't get each email).
