Theme

Flows

A flow is an event-triggered automation, stored as one validated JSON definition. A person enters a flow and exits it, and each pass is a run.

Flows are edited in the UI and through the flow API — the same operations, so your agent can build, check, rehearse and ship a flow without a deploy.

Triggers

A flow starts when an event matches. Events come from your own site or app through the event API, and from Mimeo itself: a tag being added, a field changing, a sequence completing are all ordinary events. That's why "when someone gets tag X" needs no special support.

Beyond the event name, a trigger can match on the event's details, with and/or nesting:

"trigger": {
  "event": "signed_up",
  "match": {
    "all": [
      { "detail": "plan", "op": "exact", "value": "trial" },
      { "any": [
        { "detail": "source", "op": "contains", "value": "podcast" },
        { "detail": "ref", "op": "has_value" }
      ] }
    ]
  }
}

Operators: exact, contains, has_value, is_empty, greater, less. Numbers compare as numbers, dates as dates, text case-insensitively.

Flows trigger on events only. There are no time-based triggers. Waiting is something a flow does, not something that starts one.

Steps

The definition is a nested list of steps, and the order they're written in is the order they run. A branch keeps its two sides inside itself; when a side runs out, the person carries on to the step after the branch. There are no pointers between steps, so the JSON has the same shape as the diagram.

Every step has a number that is assigned once and never reused, and a readable slug that follows its name. Together they make the step's handle (7-send-pitch), and with the flow's key, its step ID (trial_journey.7-send-pitch) — which shows up everywhere: the visualization, run histories, dry-run traces, and the API. Rename a step and the readable half changes; the number doesn't, which is what keeps the people currently standing on it standing on it.

Step What it does
send_emailQueues an email. Flow sends go through the queue like everything else, so guards, send windows and pacing all apply.
start_sequenceStarts a sequence and always waits for it to finish.
add_tag / remove_tagChanges tags. Fires the usual system event, so other flows can trigger on it.
set_fieldWrites a custom field, recording the old and new values.
delayWaits a set length, or a named duration from Settings.
gateWaits until something becomes true. Give it an expiry and it branches: one path for the ones who made it, one for the ones who gave up.
fire_eventEmits an event of your own naming.
if_elseBranches on a condition. Both paths meet again below unless one ends in an exit.
exit_flowEnds the run here.

There is deliberately no cancel-scheduled step. Stopping an email that's already queued is what guards are for, and having one job in one place is what keeps "why didn't that send?" answerable.

Starting a sequence always waits

The person stays at that step until their sequence run finishes. There's no fire-and-forget option, because two copies of the same person moving through one flow at once is never what anyone means.

They're released by the sequence completing, the run being canceled (a cancellation is a way of finishing), or someone advancing them by hand from their person page.

If they can't start it — the sequence is off, or it's single-run and they've had their run — they pass straight through with no waiting, and the reason is written into the run's history: "skipped Welcome — already completed 2026-03-14".

Gates

A gate waits for something to become true. If it already is, the person walks straight through with no wait at all.

A gate that gives up gets two paths — Criteria met for the ones who did the thing, Gave up for the ones still waiting when the time ran out. Leave a path empty and those people carry straight on below; end one with an exit and they leave for good. Otherwise both paths meet again under the gate, exactly as an if_else's do.

With no expiry they wait indefinitely, so nobody can ever give up and the gate has no paths of its own — it is a plain wait that happens to be watching for something. Sometimes right, but it should be on purpose, and the validator warns either way.

Waiting on a named duration

A delay can name a duration instead of hard-coding a number: { "setting": "quiet_buffer_days" }. The number lives in Settings → Sending. Changing it changes every flow that waits on it, immediately, without touching a definition and without creating a new version — because the flow didn't change. People already waiting keep the length they started with.

Conditions

The same vocabulary is used by if_else, gates, and guards, so a condition means exactly the same thing wherever it's written. They compose with all, any and not.

Named code predicates

The condition list stays small on purpose: a settings screen that grows into a programming language serves nobody. When you need real logic, write real Ruby in config/initializers/flow_predicates.rb and reference it by name:

{ "type": "predicate", "name": "high_value_at_risk", "args": { "months": 6 } }

The block receives the person and the args and returns true or false. A predicate that raises is treated as false and logged, so a broken plugin never stops the engine. Registered predicates validate in flow definitions, so an agent gets a real error for a typo rather than a silently-false condition.

Runs

The flow page

Clicking a step opens an editor for what it is and what it does: its name, its handle, which email, how long, which tag, what a field becomes, what a branch splits on. Where it sits is changed on the diagram itself, where you can see what you're changing — deleting a branch takes everything inside it, so the confirmation lists exactly what goes and warns if anyone is standing on it.

None of that goes away when you'd rather describe a change than click it. The same flow is editable through the API, which is what your coding agent uses — and it's still the better tool for the changes that are awkward to click: restructuring a branch, rewriting the same condition across six steps, or building a flow from a brief. Either way the whole definition is validated as one piece before it can go live.

The up/down controls move a step among its neighbours — the steps beside it in the same list, whether that's the main flow or one side of a branch. To get a step out of a branch, "move out" lifts it one level, to just above or just below the branch it was inside; repeat it to climb further.

Going live

Activating checks the definition first. Errors block it — a step pointing nowhere would strand people. Warnings don't: a gate with no expiry, or a tag that doesn't exist yet, is a choice.

Pausing is instant and needs no confirmation, because it's what you reach for when something is wrong. Nobody new enters, nobody already inside advances, and the emails already queued for them hold — including those of a sequence the flow started. Un-pausing releases them.

Versions

Checkpoints are yours: name one when a definition works and you don't want to lose it. Activation versions are automatic — every time a flow goes live, what went live is recorded, so there's always a known-good definition to roll back to.

Rolling back a live flow re-checks the old definition first: a version that was valid when you saved it can reference an email that's since been archived, and restoring it blindly would break sends rather than fix them.

People mid-run always follow the flow's current definition — there's one live version of a flow, not one per person, which is what makes "fix the typo and it's fixed for everyone" true. If someone is parked on a step an edit removes, they exit rather than waiting forever, and the exit says why.

Dry run

A flight simulator for the flow. Pick a real person and an event, and watch what would happen to them — with their real tags, fields, purchases and history.

Nothing happens. No email is queued, no tag is added, no run is created. It's a separate read-only walk through the definition, not the engine with a switch flipped.

The trace shows the trigger check with the values it actually saw, each condition with its real answer ("has an active subscription to pro-plan → no (status: cancelled)"), the branch taken, and every action as a would-statement. It walks straight through delays — what matters is what happens, not how long it takes — and stops at the first place the person would actually be waiting.

Loops

A flow can fire an event, and that event can trigger another flow. That's the point. A flow whose own fire_event matches its own trigger would run forever, so every event carries how many engine hops produced it and the chain stops after 25. The validator warns about the loops it can see; the hop cap is the backstop for the ones it can't (A fires B, B fires A).