Flows & steps
A flow is a URI and a list
A flow has a base URI — funnels/spring — and an ordered list of steps. Each step gets a slug, and
is served at funnels/spring/<slug>. A hit on the base URI itself sends the visitor to the first
step.
A flow can be scoped to one site or served on all of them. Routes are built from the database, so renaming a slug moves the page immediately.
The six step types
| Type | What it is | Reachable when |
|---|---|---|
| Landing | An ordinary page at the front of the funnel | always |
| Opt-in | Collects an email onto the cart before the checkout | always |
| Checkout | Stocks the cart and shows any order bumps | always |
| Upsell | A post-purchase offer, charged with one click | the order is complete and paid, the offer is eligible, the window is open |
| Downsell | The offer shown after the first is declined | the same, for its own offer |
| Thank you | The end of the funnel | the order is complete |
Upsell and downsell are Pro. A Lite install will not serve them even if the rows exist.
Guarding
Every request goes through one resolver, so there is exactly one answer to "may this visitor see this page". A visitor who types a thank-you URL before ordering is not shown an empty page or an error — they are walked forward to the first step they can see.
The same resolver answers the offer endpoints, which is why a hand-crafted POST cannot charge an offer whose window has closed. The button and the charge ask the same question.
Stocking the cart
A checkout step lists what the funnel sells, by SKU. Cart Mode decides what happens to anything already in the basket:
- Add to the cart — the funnel's products go in alongside whatever was there.
- Replace the cart — the funnel sells only its own products.
- Leave the cart alone — the step sells whatever the visitor arrived with.
Stocking happens once per session. A visitor who reloads the checkout, or comes back to it from a failed payment, does not end up with two of everything.
SKUs are used rather than an element picker on purpose: not every Commerce purchasable is a product variant, and a SKU means the same thing in every environment — which is what makes export and import safe.
Where "next" goes
By default, the next enabled step in order — skipping any the visitor cannot see. That is how a declined upsell falls through to its downsell and on to the thank-you page without anyone wiring the chain by hand.
Pro adds conditional routes. A step can carry a list of {"stepHandle": "…", "condition": {…}}
evaluated in order when the visitor leaves it; the first match wins. The condition is a Commerce
order condition, plus one rule Commerce does not have: Customer's Previous Orders, which counts
completed orders excluding the one being evaluated — the exclusion is the whole point, or every
first-time buyer looks like a returning one.
A flow can also carry entry rules, which decide where a hit on the base URI lands. A returning customer can skip the landing page and go straight to the checkout.
Sessions
A visitor's place in a funnel lives in a Fjord session, keyed by a cookie — not Craft's user session, because a funnel has to work for someone who has never logged in. It carries the cart, the completed order, the split-test arm they were given, which steps have stocked the cart, and the payment context captured during checkout.
Sessions expire after the configured lifetime (72 hours by default) and are pruned by garbage collection. Their recorded events survive: the analytics are the reason the sessions existed.
The store checkout
One flow can be marked Store Checkout. That does not intercept any of Commerce's URLs — a Craft
store owns its own checkout path and may have put it anywhere. Instead the flow becomes what
craft.fjord.storeCheckoutUrl points at, so a cart template can write:
<a href="{{ craft.fjord.storeCheckoutUrl ?? url('shop/checkout') }}">Checkout</a>
and work whether or not a funnel has taken it over. Marking a second flow as the store checkout demotes the first, so the store never has two.