A journey looks simple in the editor: boxes, arrows, an email or two. Underneath, it's a long-running program with one instance per person — some instances minutes old, some sleeping for months — each deciding, independently, when its person should hear from you next. This post walks the actual decision machinery in our journey engine: how people get in, the three kinds of time it understands, how it waits for behavior, and how it decides to stop.
TL;DR: A journey is a graph of typed nodes, and a person's progress through it is durable state. Two node types let people in (segment membership or a named event). Delays come in three flavors — fixed duration, the recipient's local time, or a date stored on their profile. The most interesting node waits for behavior with a timeout, so "did they book?" and "it's been a week" are two branches of one decision. Exits are explicit, and everything between entry and exit survives restarts, deploys, and months of sleeping.
The shape: a graph where every person is their own execution
The engine's data model is a directed graph of typed nodes — entries, messages, delays, waits, splits, exits. When a person enters, the engine starts tracking their position in that graph: not a row in a batch, but an individual, durable execution. This is the property everything else hangs on: the journey that emails "three days after booking" must remember each person's third day separately, keep remembering through deploys and restarts, and wake up on time even if "on time" is in October. Durability isn't an optimization here — it's the product. An engagement engine that forgets sleeping users is just a slow random number generator.
Two doors in: membership and moments
People enter a journey through one of two entry node types, and the distinction maps exactly to two kinds of marketing intent:
- Segment entry — "people who are something": the journey admits whoever belongs to a segment, and as people newly qualify, they enter. The node carries one notable option: whether a person who leaves and re-qualifies may re-enter. That flag is the difference between a one-time welcome and a recurring win-back, and making it explicit forces the journey author to decide instead of discover.
- Event entry — "people who did something": the journey starts the moment a named event arrives — a form submission, a purchase. The event's name is the trigger contract (this is the machinery behind forms that feed themselves: each form fires an event named for it, and a journey listens for exactly that name). Event entries can also carry a key, so the same person can hold multiple concurrent executions keyed to different events — two separate orders each get their own post-purchase sequence, instead of the second clobbering the first.
Three kinds of time
"Wait three days, then send" sounds like one feature. Shipped reality needs three, and the delay node supports each as an explicit variant:
| Variant | Waits until | The job it does |
|---|---|---|
| Duration | N seconds from now | "Three days after the welcome" — relative cadence |
| Local time | A wall-clock moment in the recipient's day | "9am their time" — so a national audience doesn't get 3am email |
| Profile date | A date stored on the person, ± an offset | "Two days before their appointment" — anchored to their data, not the send date |
The third variant is the quietly powerful one: the wait target lives on the person's profile, which means the journey can be authored once and each execution resolves its own deadline — the reminder journey doesn't know when appointments are; each person's instance does. It's also why record hygiene is load-bearing: a delay anchored to a profile date inherits that date's correctness.
Waiting for behavior: the two-branch decision
The node we find most architecturally interesting is the wait-for: it pauses the execution until the person enters a given segment — or a timeout elapses — and routes to a different child branch in each case. That one primitive expresses the entire family of "respond to what they do, not just to time passing":
- "Send the quote follow-up only if they haven't accepted within a week" — the segment is accepted-the-quote; the timeout branch is the nudge; the segment branch is a graceful exit.
- "If they book within three days, send prep info; otherwise send the gentle reminder" — both outcomes are first-class branches, authored together.
The design point worth stealing: condition and timeout are one node, not two competing mechanisms. Engines that bolt "exit early if X" onto a linear sequence accumulate special cases; making wait-for-behavior a primitive with an explicit timeout child means every "did they / didn't they" is just another fork in the graph — visible, testable, and impossible to forget to handle, because the timeout branch is structurally required.
Splits, throttles, and the explicit exit
Three more node families round out the decision vocabulary. Segment splits branch on who the person is right now (member / not member), which is how one journey serves two audiences without duplicating itself. Experiment splits divide traffic by percentage — A/B testing as a structural node rather than a reporting afterthought, so the variants are visible in the graph itself. Rate limits throttle how fast a node lets executions through — the guard that keeps "message everyone who qualified overnight" from becoming a 6am burst that looks like spam to every receiving mail server at once.
And then the exit — an explicit node, not just the absence of arrows. Reaching it ends the execution cleanly; the engine knows the difference between "finished" and "stuck," which is what makes journey reporting trustworthy: a journey's numbers can say how many people are in each stage precisely because being-in-a-stage is real state, not an inference from logs.
What we'd tell anyone building one of these
- Make per-person state durable first. Everything users love about journeys — months-long patience, their-time delivery, behavior-aware branching — is downstream of executions that survive anything. Build on workflow machinery that checkpoints; don't reinvent sleep with cron and hope.
- Type the nodes. A typed graph (this node is a delay; its variant is local-time) is what makes journeys editable by UI, draftable by AI, and validatable before they run — the same artifacts-over-configuration argument as everywhere else in our stack.
- Force the timeout branch. The wait-for node's best feature is what it won't let you do: wait for behavior without deciding what happens when it doesn't come. Defaults like that are product opinions encoded as schema — the cheap place to encode them.
- Name events like contracts, because they are. An event entry binds to an event name forever; renaming the form quietly orphans the journey. Treat event names with the same respect as URLs — a promise someone else built on.
Key takeaways
- A journey is one program, many executions: each person's position in the graph is durable state that survives deploys and sleeps for months — durability is the product, not an optimization.
- Two entry semantics: segment entry admits people who are something (with an explicit re-enter flag); event entry fires on what they did, optionally keyed so concurrent executions don't clobber each other.
- Time comes in three flavors: fixed duration, the recipient's local clock, and dates stored on their profile with offsets — the last one lets one journey resolve a different deadline per person.
- Wait-for-behavior is one node with two branches: the segment child and the timeout child are authored together, so "didn't respond" can never be the forgotten case.
- Splits, experiments, and rate limits are structural: branching, A/B variants, and send throttling live in the graph where they're visible — not in reporting afterthoughts or infrastructure folklore.
- Exits are explicit: the engine distinguishes finished from stuck, which is the foundation under every trustworthy journey report.
Frequently asked questions
What happens to people mid-journey when the journey is edited?
This is the classic operational question for any engine like this, and the honest general answer is: in-flight executions and graph edits have to be reconciled carefully — which is why we treat journey changes the way we treat schema changes, not copy edits. The safe author-side habits: prefer adding branches over rewriting the path people are standing on, and when a journey needs structural surgery, let the old version drain while the new one takes entries. The product-side advice we give in the journeys guide — reread your journeys quarterly — exists partly so these edits happen deliberately rather than mid-incident.
Why segments as the condition language, instead of inline rules on each node?
Reuse and consistency. A segment is a named, shared definition — "has accepted a quote," "is a member" — maintained once and referenced everywhere: entries, splits, wait-for conditions. Inline rules drift apart node by node until two branches disagree about what "active customer" means. Naming the condition also makes journeys legible to non-builders: the graph reads as sentences about people, not as embedded query syntax.
How does the engine avoid double-sending if something retries?
The general principle — ours and any well-built engine's — is that message sends are recorded as part of the execution's durable state, so a retry resumes after the recorded step rather than re-running it. It's the same lesson as our distributed-writes postmortem wearing different clothes: the dangerous design is the one where "did it happen?" is answered by hope. Make the step's completion a fact in the state, and retries become safe by construction.
Is this overkill for a small business sending a welcome series?
The machinery is invisible at small scale and load-bearing at every scale: the welcome series for a five-person shop still needs per-person timing, still must not double-send, and still has a "they converted mid-sequence" case — the wait-for node's whole job. The difference between toy and real engines isn't features; it's whether the edge cases are structural or someone's 2am surprise. Small businesses deserve the structural version — that's rather the point of our platform.
The journey engine ships inside Faster — authored from a description, triggered by your forms, and patient on every customer's clock. More engineering notes: the engineering blog.