Slice-1: linear flow components via the defflow macro.
We deliberately shadow clojure.core/await (the agent-blocking
primitive nobody reaches for in 2026) with our own coroutine-suspending
await. The :refer-clojure :exclude keeps the compiler quiet.
A flow is a component whose role is to sequence a series of child
components and return a final value. Written by hand, a flow looks
like a state machine: a :start hook plus a chain of :on-step-N
resume keys that thread the partial result forward. That is correct
but tedious; the same logic reads naturally as straight-line code:
(s/defflow :booking/wizard []
(let [dates (s/await (s/embed :booking/dates))
room (s/await (s/embed :booking/room {:dates dates}))
receipt (s/await (s/embed :booking/pay {:price (price-for room)}))]
{:dates dates :room room :receipt receipt}))
The defflow macro compiles that body into a regular component
registration. Under the hood we use cloroutine:
the body becomes a stackless coroutine whose suspend point is
await, and the coroutine continuation lives on the instance map as
::coro. Each user event delivered by the kernel is funnelled into
one fixed resume key (:on-flow-resume) that injects the child's
answer back into the coroutine and steps it forward.
The result is a component whose externally observable behaviour is
identical to a hand-rolled chain of :on-step-N callbacks (modulo the
resume key's name; see §13 of v2_1.md), but whose source reads as
ordinary Clojure.
────────────────────────────────────────────────────────────────────── Restrictions inherited from cloroutine ──────────────────────────────────────────────────────────────────────
await cannot appear inside a nested fn, lazy seq, custom type
method, or anywhere else the surrounding form might escape the
coroutine's synchronous context. let/do/if/cond/when/
loop+recur are all fine.try/catch across an await is not supported in slice 1 (open
question, see v2_1.md §13).Slice-1: linear flow components via the [[defflow]] macro.
We deliberately shadow `clojure.core/await` (the agent-blocking
primitive nobody reaches for in 2026) with our own coroutine-suspending
`await`. The `:refer-clojure :exclude` keeps the compiler quiet.
A *flow* is a component whose role is to sequence a series of child
components and return a final value. Written by hand, a flow looks
like a state machine: a `:start` hook plus a chain of `:on-step-N`
resume keys that thread the partial result forward. That is correct
but tedious; the same logic reads naturally as straight-line code:
(s/defflow :booking/wizard []
(let [dates (s/await (s/embed :booking/dates))
room (s/await (s/embed :booking/room {:dates dates}))
receipt (s/await (s/embed :booking/pay {:price (price-for room)}))]
{:dates dates :room room :receipt receipt}))
The `defflow` macro compiles that body into a regular component
registration. Under the hood we use [cloroutine](https://github.com/leonoel/cloroutine):
the body becomes a stackless coroutine whose suspend point is
[[await]], and the coroutine continuation lives on the instance map as
`::coro`. Each user event delivered by the kernel is funnelled into
one fixed resume key (`:on-flow-resume`) that injects the child's
answer back into the coroutine and steps it forward.
The result is a component whose externally observable behaviour is
identical to a hand-rolled chain of `:on-step-N` callbacks (modulo the
resume key's name; see §13 of `v2_1.md`), but whose source reads as
ordinary Clojure.
──────────────────────────────────────────────────────────────────────
Restrictions inherited from cloroutine
──────────────────────────────────────────────────────────────────────
* `await` cannot appear inside a nested `fn`, lazy seq, custom type
method, or anywhere else the surrounding form might escape the
coroutine's synchronous context. `let`/`do`/`if`/`cond`/`when`/
`loop`+`recur` are all fine.
* `try`/`catch` *across* an `await` is not supported in slice 1 (open
question, see `v2_1.md` §13).
* Storing the coroutine on the instance map gives up strict EDN
serialisability for flow instances; persistence (slice 3) will
treat them as a separate concern.(-advance self answer)Run self's coroutine once, injecting answer (or nil for the
first step), and return [self effects] for the kernel.
Public-by-namespace-convention only — the macro emits calls to it. Application code should not call this directly.
Run `self`'s coroutine once, injecting `answer` (or `nil` for the first step), and return `[self effects]` for the kernel. Public-by-namespace-convention only — the macro emits calls to it. Application code should not call this directly.
(await embed-spec)Inside a defflow body, suspend the surrounding flow until the
embedded child produced by embed-spec answers, then resume with the
child's answer value.
Calling await outside a defflow body has no useful meaning; it
exists primarily as the cloroutine break-point marker.
Inside a [[defflow]] body, suspend the surrounding flow until the embedded child produced by `embed-spec` answers, then resume with the child's answer value. Calling `await` outside a `defflow` body has no useful meaning; it exists primarily as the cloroutine break-point marker.
(defflow id bindings & body)Define and register a flow component.
(s/defflow :booking/wizard [{:keys [user-id]}]
(let [dates (s/await (s/embed :booking/dates {:user user-id}))
room (s/await (s/embed :booking/room {:dates dates}))]
{:dates dates :room room}))
id — a namespaced keyword (same rule as defcomponent).bindings — a destructuring vector applied to the embed args map
passed at instantiation. [] is fine if the flow takes no args.body — ordinary Clojure that may call await zero or more
times. The body's final expression is the value the flow answers
to its parent (or, for a root flow, the value of :end).Restrictions are documented at the top of dev.zeko.stube.flow.
Define and register a flow component.
(s/defflow :booking/wizard [{:keys [user-id]}]
(let [dates (s/await (s/embed :booking/dates {:user user-id}))
room (s/await (s/embed :booking/room {:dates dates}))]
{:dates dates :room room}))
* `id` — a namespaced keyword (same rule as `defcomponent`).
* `bindings` — a destructuring vector applied to the embed args map
passed at instantiation. `[]` is fine if the flow takes no args.
* `body` — ordinary Clojure that may call [[await]] zero or more
times. The body's final expression is the value the flow answers
to its parent (or, for a root flow, the value of `:end`).
Restrictions are documented at the top of [[dev.zeko.stube.flow]].(flow-cdef id init-fn)Build the component map a defflow registers. Pulled out of the
macro so the structure is easy to read and easy to test. Public so
macro expansions in user namespaces can call it.
Build the component map a `defflow` registers. Pulled out of the macro so the structure is easy to read and easy to test. Public so macro expansions in user namespaces can call it.
(on-resume)Cloroutine resume hook: returns the value that the suspended await
call should evaluate to inside the body. Always invoked synchronously
by (coro) from inside step!, so *answer* is in scope.
Public so the defflow macro can name it from the user's namespace;
not intended for application code.
Cloroutine resume hook: returns the value that the suspended `await` call should evaluate to inside the body. Always invoked synchronously by `(coro)` from inside [[step!]], so `*answer*` is in scope. Public so the `defflow` macro can name it from the user's namespace; not intended for application code.
The single resume key under which every flow's child answers are delivered. Exposed so tests and tooling can refer to it without hardcoding the keyword.
The single resume key under which every flow's child answers are delivered. Exposed so tests and tooling can refer to it without hardcoding the keyword.
(step! coro answer)Advance coro (a cloroutine continuation) by one fragment.
answer is the value the previously suspended await call should
evaluate to; pass nil for the very first step (which has no
suspended await to resume).
Returns one of:
[:yield <embed-spec>] the body suspended at `(await embed-spec)`
[:done <value>] the body ran to completion with `value`
Advance `coro` (a cloroutine continuation) by one fragment.
`answer` is the value the previously suspended `await` call should
evaluate to; pass `nil` for the very first step (which has no
suspended await to resume).
Returns one of:
[:yield <embed-spec>] the body suspended at `(await embed-spec)`
[:done <value>] the body ran to completion with `value`cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |