Liking cljdoc? Tell your friends :D

dev.zeko.stube.flow

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 ADR 0001-resume-key-naming), 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: a catch in the surrounding body cannot intercept the exception thrown into the coroutine on resume. Use dev.zeko.stube.core/answer-error in the child to route failures explicitly.
  • Storing the coroutine on the instance map gives up EDN serialisability for flow instances. A conversation containing a defflow is therefore not durable — the dev.zeko.stube.store file store logs a warning and skips its on-disk save. This is a deliberate property: defflow is the ergonomic for transient flows. When you need a long-running flow that survives restarts, write a hand-rolled task component with :start plus named resume keys. The tutorial section Durable flows: defflow vs. task components shows the same wizard in both shapes.
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 ADR
[0001-resume-key-naming](../../../../docs/decisions/0001-resume-key-naming.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: a `catch` in
  the surrounding body cannot intercept the exception thrown into
  the coroutine on resume.  Use [[dev.zeko.stube.core/answer-error]]
  in the child to route failures explicitly.
* Storing the coroutine on the instance map gives up EDN
  serialisability for flow instances.  A conversation containing a
  `defflow` is therefore not durable — the [[dev.zeko.stube.store]]
  file store logs a warning and skips its on-disk save.  This is a
  deliberate property: `defflow` is the ergonomic for transient
  flows.  When you need a long-running flow that survives restarts,
  write a hand-rolled task component with `:start` plus named resume
  keys.  The tutorial section *Durable flows: defflow vs. task
  components* shows the same wizard in both shapes.
raw docstring

-advanceclj

(-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.
sourceraw docstring

awaitclj

(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.
sourceraw docstring

defflowcljmacro

(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 and the in-memory-only durability boundary 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 and the in-memory-only durability boundary are
documented at the top of [[dev.zeko.stube.flow]].
sourceraw docstring

done-tagclj

source

flow-cdefclj

(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.
sourceraw docstring

on-resumeclj

(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.
sourceraw docstring

resume-keyclj

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.
sourceraw docstring

step!clj

(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`
sourceraw docstring

yield-tagclj

source

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close