Journeys & journey frames
A journey is a named multi-step flow the planner commits to; the frame narrows the served space to the journey's steps plus a guaranteed escape.
A journey is a named multi-step flow: does (the planner text), steps (action ids by
qualified path or unambiguous suffix, resolved loudly at build time), and an optional when
precondition. Journeys are what Mode B serves as fixed tools — one tool per journey,
forever.
It is also the one relational thing you declare. A preferred order toward a goal is meaning, and meaning is yours. Everything else relational — a step's dependencies, what would free a greyed control, the route to a page — is DERIVED from declarations you already made for other reasons, so none of it can drift from your graph.
Frames — on-demand disclosure
Opening a journey (commitJourney, or calling its Mode B tool with no step) opens a frame:
the served action space narrows to the frame's steps plus authored cancel/back plus the
synthetic leave-journey escape — guaranteed for every frame that opens, so a committed
planner is never locked in. The frame's result discloses readySteps (fireable now),
laterSteps (with status), and awaitingState (steps whose fire has not yet been reported
back — see the settlement rule).
journeyPlan(id) returns the derived dependency DAG with live per-step status — and THROWS on
an unknown id, because every in-library caller passes an id the spec just yielded.
tryJourneyPlan(id) is the same plan for an id you did not author (a model's, a URL's):
{ ok: true, plan } or { ok: false, reason: 'UNKNOWN_JOURNEY', known } — the identical
failure shape commitJourney() returns, so a caller holding a model-supplied id handles the
question one way.
Where a journey stands
journeyPlan answers what may I fire next. The question a reader actually has between turns is
a different one: whose turn is it, and is this thing moving. session.journeyStanding(id)
answers that with one word and the facts behind it.
const where = .('buy');JourneyStanding is a pure fold over the plan, the ask book, the
decisions book, retained settlements and frame history: no state of its
own, no cache, no timer, and it never fires. It computes fresh on every call, so the word is true
about now. It throws on an unknown id, exactly as journeyPlan does and through that method's
own refusal — serving layers resolve names first.
An open frame governs; otherwise a latest-closed 'completed' frame answers 'done', and a
cancelled or demoted one contributes history and never a verdict. Then the steps are walked in
chain order, and the first one not done — the governing step — names the standing:
standing | what holds it | evidence carries |
|---|---|---|
'awaiting-human' | the governing step's card is open | askId, step |
'declined' | the human answered no through their own door | askId, step |
'with-the-human' | the decision belongs to a person | step, made, about?, madeBy? |
'failed' | its LAST attempt came to rest badly | transitionId — a pointer, never the receipt |
'blocked' | its guard was evaluated and failed | blockedOn |
'in-progress' | nothing holds it | guardUnevaluated when present |
'done' | every step is done | the counts |
Every arm also carries stepsDone and stepsTotal, so a journey nobody has started reads
'in-progress' with stepsDone: 0 — the honest reading of open, and nothing holds it.
'failed' is never minted from a pause. Not from needs-confirm, not from a relayed decline,
not from any approval refusal, not from a guard, disabled or materialization refusal. A refusal is
not an execution: nothing ran, so nothing failed. It requires a fire that actually came to rest
badly.
The strings live on this type alone — no existing union grew for them. Both Mode B doors serve the
word from this same call (whats_here's journey rows and the journey tool's result), never a
second derivation, so two doors cannot disagree about one chain. It sits beside judgment, which
answers the other question: judgment is what is my move this turn, standing is where does
this chain stand.
The per-step carrier
A step is a name — or the object element { step: 'place-order' }, which compiles identically.
steps: ['enter-address', { step: 'choose-shipping-speed' }, 'place-order']It carries nothing beyond step today. It exists because per-step conditional metadata has to have
exactly one authoring carrier: deciding it once means the next such feature lands as a new
optional field on a shape that already exists, rather than as a second shape competing with this
one. humanDecides is deliberately not one of them — ownership is a fact about the control.
The commit gates — five typed refusals
commitJourney() refuses, in order, with a typed reason:
UNKNOWN_JOURNEY— withknown, the list of real ids;STALE_CURSOR— the world moved since the caller looked;FRAME_ALREADY_OPEN— one committed flow at a time;PRECONDITION_FAILED— the journey'swhenfails against projected state;ENTRY_NOT_MATERIALIZED— the never-trap gate. An agent commit outside a tour session is refused when the entry step could not act AT ALL right now: no registered handler under any key (instance-keyed wiring on a repeats container counts — the fire that follows carries the instance) and no navigate-derived gesture. The refusal carries the entry step'saffordanceIdand, when declared, itsgesture. The frame that could never act is never opened.
The fifth gate asks the same widened materialisation question fire() asks — one code
path, never a second lookup — so the commit gate and the fire gate can never disagree. See
Actuation & materialisation.
If your agent flow commits journeys before the app's handlers mount: register the entry step's
handler first (the same wiring 0.3.0 asked of fires), pass the navigate option when the
entry is a pure navigation, commit with source: 'user' for human-driven flows, or create
the session with allowUnmaterializedFires for touring.
Only the ENTRY_NOT_MATERIALIZED refusal lands a gap-ledger
row — it is a capability gap (a binding to build). The other four are protocol events, not
capability gaps, and stay un-ledgered.
The journey list you already own
If your app already keeps journey definitions, fromJourneys reads them in
this same does / steps / when vocabulary — one authoring language, compiled by the same
pass, refused in the same voice.
A note on the word
Before 1.0 this was called a skill, and the compiled shape was Skill. The agent ecosystem has
since settled "skill" on a packaged capability an agent loads, and a library that keeps a private
meaning for that word spends every conversation explaining which one it meant. What this describes
is the path a person takes through an app — a journey. The old spellings are gone, not
deprecated: skills: no longer type-checks, and a definition that reaches the runtime with one
is refused by name.
The navigation graph
buildNavigationGraph turns the container tree you already picture — pages, areas, tabs, modals, tools — into a validated, frozen graph with typed node paths.
Guarded journeys
A wizard whose steps sit behind guards, whose Next button is greyed rather than hidden, whose every step proves it happened, and whose route table doubles as the spine that keeps every page reachable.