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_email | Queues an email. Flow sends go through the queue like everything else, so guards, send windows and pacing all apply. |
start_sequence | Starts a sequence and always waits for it to finish. |
add_tag / remove_tag | Changes tags. Fires the usual system event, so other flows can trigger on it. |
set_field | Writes a custom field, recording the old and new values. |
delay | Waits a set length, or a named duration from Settings. |
gate | Waits 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_event | Emits an event of your own naming. |
if_else | Branches on a condition. Both paths meet again below unless one ends in an exit. |
exit_flow | Ends 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.
-
Waiting for an event. Two modes:
next_occurrencemeans an earlier occurrence doesn't count and they have to do it again;any_pastmeans someone who already did the thing passes through immediately, so you don't make people prove themselves twice. - Waiting for a condition. Re-checked whenever anything about the person changes, and once a minute besides — so things no event covers (an open, a click, a subscription lapsing on its own renewal date) still open the gate.
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.
- Field compare — equals, contains, greater, less, empty, has any value
- Has a tag
- Is unsubscribed
- An event happened, optionally with detail matching and within N days
- Engagement — received, opened, or clicked a specific email
- Run history — started or finished a sequence within N days; completed a flow
- Money — has an active subscription to a product; bought a product
- Segment membership — is in segment X
- Total spend — lifetime spend over or under an amount
- A named code predicate
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
- A flow's run policy is either single run ever or repeat runs allowed.
- A person can have at most one active run per flow, and can be in many different flows at once.
- Runs record when the person entered, when they exited, and where from — the end of the flow, an
exit_flowstep, a guard, a manual advance, or a step that no longer exists. - Exits appear on the person's timeline.
- People waiting wait in a visible row with a wake time on it, not in a queued job. You can always see who is waiting and until when.
The flow page
- Visualization — the definition drawn as a diagram, with every step named, handled, and carrying a live count of how many people are sitting at it. A count on a step nobody should be at is usually the first sign something is wrong. Everything about the flow's structure can be changed here: the
+between any two steps adds one, and each step's controls move, nest, un-nest and delete it. - JSON — the whole definition, with a check-it button that reports problems by step. Saved on request rather than autosaved, because mid-edit JSON is routinely invalid.
- Side by side — diagram and JSON together, nested the same way, so the JSON reads as the picture written down. Selecting a step in one highlights it in the other: pick a branch and it highlights along with everything inside it, pick one of those inner steps and it highlights alone. Every step copies on its own — subtree included — straight into an agent prompt.
- Versions — see below.
- Dry run — see below.
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).