# Flows API

Build, check, rehearse and ship a flow without a deploy. Full parity with the UI — this is the API the CLI and MCP server reuse.

All endpoints need a bearer token (see [Overview & auth](/content/docs/api/index.html)). Flows are addressed by **id or key**, so an agent that wrote `welcome_series` doesn't have to look up a number to edit it.

## The working loop

1. `GET /api/v1/emails` and `/sequences` to resolve the IDs the definition needs.
2. `POST /api/v1/flows` to create a draft, or `PATCH` an existing one.
3. `POST .../validate` until it comes back clean. A definition can be _saved_ while it still has problems, which is what makes iterating possible.
4. `POST .../dry_run` against a real person to see what would happen.
5. `POST .../activate`. This refuses on errors and records a version automatically.

## Endpoints

| Method & path | What it does |
| --- | --- |
| `GET /api/v1/flows` | Every flow with its labels, trigger summary, step count, and how many people are inside, plus `labels`: every label in use. Narrow to one group with `?label=`. |
| `POST /api/v1/flows` | Create a draft. Body: `key`, `name`, `labels`, `run_policy`, `definition`. |
| `GET /api/v1/flows/:id_or_key` | The full definition, every step's ID and one-line summary, live per-step counts, and the current validation result. |
| `PATCH /api/v1/flows/:id_or_key` | Update `name`, `labels`, `run_policy`, or `definition`. A field you leave out is left alone. |
| `DELETE /api/v1/flows/:id_or_key` | Only while nobody has ever run through it. Otherwise `409` — pause it instead, so their history survives. |
| `POST .../validate` | Check a definition without saving. Pass `definition` to check an unsaved one, or omit it to check the saved one. |
| `POST .../dry_run` | Rehearse against a real person. Zero side effects. |
| `POST .../activate` | Go live. `409` with the validation attached if it isn't clean. |
| `POST .../pause` | Freeze the flow and hold its queued emails. |
| `GET .../versions` | Every checkpoint and activation version. |
| `POST .../versions` | Save a checkpoint. Body: `name`, `description`. |
| `POST .../versions/:version_id/rollback` | Restore a version. Re-validated first when the flow is live. |

### Lookups

Read-only, so a definition can reference real records instead of guesses.

| Path | Returns |
| --- | --- |
| `GET /api/v1/emails` | Active emails with labels and where each is used. Supports `?q=` and `?label=`. |
| `GET /api/v1/sequences` | Sequences with their steps, so you can see what a `start_sequence` step would actually do. |
| `GET /api/v1/tags` | Tags with people counts. Tags are created on demand, so this is for spelling rather than permission. |
| `GET /api/v1/products` | Product keys for money conditions. |
| `GET /api/v1/queue` | What's about to be sent, what's holding, and why. Filter with `?status=` or `?person=`. |

## Creating a flow

```
curl -X POST https://your-mimeo.com/api/v1/flows \
  -H "Authorization: Bearer $MIMEO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{\
    "key": "trial_journey",\
    "name": "Trial journey",\
    "labels": ["onboarding", "revenue"],\
    "run_policy": "single_run",\
    "definition": {\
      "trigger": {\
        "event": "signed_up",\
        "match": { "all": [{ "detail": "plan", "op": "equals", "value": "trial" }] }\
      },\
      "nodes": [\
        { "id": 1, "slug": "check-member", "name": "Already a member?", "type": "if_else",\
          "condition": { "type": "has_tag", "tag": "member" },\
          "then": [\
            { "id": 2, "slug": "member-exit", "name": "Done — already a member", "type": "exit_flow" }\
          ],\
          "else": [] },\
        { "id": 3, "slug": "start-welcome", "name": "Start the welcome sequence",\
          "type": "start_sequence", "sequence_id": 3 },\
        { "id": 4, "slug": "wait-buffer", "name": "Quiet buffer", "type": "delay",\
          "duration": { "setting": "quiet_buffer_days" } },\
        { "id": 5, "slug": "wait-for-visit", "name": "Wait for a pricing visit", "type": "gate",\
          "wait_for": { "event": { "name": "visited_pricing", "occurrence": "next_occurrence" } },\
          "expires_after": { "amount": 14, "unit": "days" },\
          "met": [],\
          "expired": [\
            { "id": 6, "slug": "quiet-exit", "name": "Done — went quiet", "type": "exit_flow" }\
          ] },\
        { "id": 7, "slug": "send-pitch", "name": "Send the pitch", "type": "send_email", "email_id": 12 }\
      ]\
    }\
  }'
```

## Labels

Labels group flows the way you think about them — `onboarding`, `revenue`, `win-back`. They are freeform, created by using them, and change nothing about how a flow runs: they exist so a list of thirty flows can be narrowed to the five that belong together.

`GET /api/v1/flows` returns each flow's `labels` alongside a top-level `labels` array of every label in use — read it before inventing a new one, so you join the existing vocabulary instead of coining a near-duplicate of it. Pass `?label=onboarding` to list only the flows carrying that label.

On `POST` and `PATCH`, `labels` replaces the whole set: send every label the flow should carry. `[]` clears them, and leaving the field out entirely leaves them alone. Blank entries and duplicates are dropped, and surrounding whitespace is trimmed.

```
# Every flow in one group
curl -H "Authorization: Bearer $MIMEO_TOKEN" \
  "https://your-mimeo.com/api/v1/flows?label=onboarding"

# Re-label one flow — this is the whole set, not an addition
curl -X PATCH https://your-mimeo.com/api/v1/flows/trial_journey \
  -H "Authorization: Bearer $MIMEO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "labels": ["onboarding", "revenue"] }'
```

Emails carry labels of their own, on the same terms — see [the emails API](/content/docs/api/emails.html). The two vocabularies are separate: a label on a flow and the same word on an email are unrelated.

## How the definition is shaped

**Order is the routing.** Steps run in the order they appear in `nodes` — the first one is where a person starts, and there are no pointers between steps to keep in sync.

**Branches nest, and both branching types have the same shape.** A branch carries exactly two arms, each a plain array of steps on the step itself: `then` and `else` on an `if_else`, `met` and `expired` on a gate whose `expires_after` is a duration. A gate that never gives up carries no arms at all — nobody can reach an "expired" path — and it is a plain wait. Spell that as an `expires_after` of "never"; omitting the field means the same but draws a validator warning, since an omission can't be told from a forgotten field.

When an arm runs out, the person carries on to the step _after_ the branch — so an empty arm means "this side does nothing extra", both arms meet again below, and an `exit_flow` at the end of an arm is how you stop the run on that side for good. Running off the end of the top-level array completes the run.

**Every step has an id, a slug and a name.** The `id` is a whole number assigned once and never reused — it is what the runs of people currently standing on a step point at, so renumbering one moves people. The `slug` and `name` are yours to change freely. Together the id and slug make the step's handle (`7-send-pitch`) and its step ID (`trial_journey.7-send-pitch`), which is what appears in the UI, logs and traces.

You may leave `id`, `slug` and `name` out and they will be filled in on save — ids continue from the flow's counter, and the name falls back to the step's type. The slug is derived from the name with the small words dropped (`"Send the upgrade pitch" → `send-upgrade-pitch`); send one yourself and it is kept as written. Supply them when you want to be sure which step is which across two writes.

Step shapes, the condition vocabulary, and what each step does are all covered in the [flows guide](/content/docs/usage/flows.html).

## Validation errors are written to be acted on

Every problem names the step, the bad value, and the fix — so an agent that reads the error can usually repair the definition on its own.

```
{  
  "valid": false,  
  "errors": [\
    {\
      "path": "nodes[4].email_id",\
      "code": "email_unknown",\
      "node": "7-send-pitch",\
      "severity": "error",\
      "message": "7-send-pitch sends email #99, which does not exist. Look up a real one with GET /api/v1/emails."\
    }\
  ],  
  "warnings": [\
    {\
      "path": "nodes[3].expires_after",\
      "code": "gate_no_expiry",\
      "node": "5-wait-for-visit",\
      "severity": "warning",\
      "message": "5-wait-for-visit has no \"expires_after\", so a person waits here indefinitely until the gate opens and the gate has no paths of its own. Add an \"expires_after\" duration if they should give up after a while, or set it to \"never\" if waiting forever is the intent."\
    }\
  ]  
}
```

**Errors block activation; warnings never do.** A step that sends a missing email would fail for real people. A gate with no expiry may well be a choice — say so with `"expires_after": "never"` and the warning goes away.

The checks run in four passes, so a later one can be trusted once the earlier one is clean:

1. **Shape** — every step has an id, a name, a slug and a known type, plus the fields that type requires, and every branch arm is an array of steps.
2. **Unique ids** — an id addresses a step for the whole life of the flow, so no two steps can share one.
3. **References** — every email, sequence, field, product, flow, segment, named duration and predicate exists. An email that's archived is an error. A tag that doesn't exist yet is a warning, since tags are created on demand.
4. **Dead code** — a step sitting after an `exit_flow` in the same list is an error, because nobody can ever reach it.

## Dry run

```
curl -X POST https://your-mimeo.com/api/v1/flows/trial_journey/dry_run \
  -H "Authorization: Bearer $MIMEO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{\
    "person": "ada@example.com",\
    "event": { "name": "signed_up", "details": { "plan": "trial" } }\
  }'
```

Pass `person` as an id or an email address. Pass a `definition` to rehearse something you haven't saved. The response reports whether the trigger matched and then one entry per step, each carrying its step ID, what would happen, and the actual values behind every condition. A branching step also carries `arm` — the path it sent them down (`then`/`else` on an `if_else`, `met`/`expired` on a gate), which is the only way to tell where a run went when that path has no steps on it. It stops at the first place the person would be waiting.

**Nothing is written.** No run is created, no email is queued, no tag is added. It's a separate read-only walk through the definition, sharing the engine's evaluators so a trace and a real run can't disagree.

## Live pages update themselves

A change pushed through this API appears on an open flow page, queue, or sequence page without a refresh. There's no extra call to make — editing the flow is what signals it.
