core.await: an in-flow leaf that holds until a named event arrives,
with an optional deadline (ADR-0002 decision 10, the 2026-09-05
amendment).
No slots, two config fields, and two outcomes. event is required and
reads the event-name grammar core.on_event and core.send already
read; timeout is an optional :duration in the one grammar
core.wait's duration and core.send's delay are written in. The
two outcomes, received and timed_out, are the two ways an await can
end.
An await is not a wait
The names are one word apart because the things are close, so this
moduledoc says which is meant rather than leaning on the verb. A
wait (core.wait) holds for a duration and ends one way. An
await holds for an event and ends one of two ways, one of which is
a duration elapsing. io/1 is core.wait's declaration byte for byte,
which is the whole of what "valid wherever a wait is" means: both are
kinds: [:step], and both leave consumes and produces to ADR-0003
decision 5's permissive default, because holding transforms no data.
Why this is a type rather than an arrangement
The nearest arrangement is a core.group whose interrupts rail
carries a core.on_event for the awaited event and, for the deadline,
the deadline recipe's core.send and a second handler. It does not
express this type, because a handler's outcome word becomes one of
exactly two package-owned interrupt events
(StatifierBlocks.Core.Emit.interrupt_events/0) and a group declares
no outcomes at all: two abandon handlers on one rail are
indistinguishable to everything downstream. This type declares the two
outcomes directly, which is the seam the arrangement is missing.
Both outcomes are declared whether or not a deadline is stored
outcomes/1 returns received and timed_out for every config,
including one with no timeout. An outcome's wiring is an event rather
than a target (ADR-0004's outcome amendment, 2c), so a parent may
transition on an outcome whose <final> was never emitted and the
transition simply never fires; nothing in the compiler cross-checks a
declaration against an emitted final either. Deriving the list from
timeout instead would take a declared seam away from an author who
cleared the field, which is the failure ADR-0002 decision 6's slots/1
stability rule exists to avoid on the slot side. The emitted bytes stay
honest either way: with no timeout, no timer is armed and no
timed_out final is written.
The deadline's timer cannot outlive the await
When timeout holds a duration, the compiled waiting state arms a
delayed <send> whose id is minted with
StatifierBlocks.Compiler.Context.role_id/2 under
StatifierBlocks.Compiler.Cancels.armed_role/0 - the reserved role
core.wait and core.send mint under. StatifierBlocks.Compiler.Cancels
therefore reaches it like any other armed send and cancels it in the
enclosing scope's <onexit>, so an await left before its deadline -
because the awaited event arrived, because an interrupt fired, because
a losing lane exited, because a group was abandoned - leaves no timer
behind. Nothing in that module changes for this type; an await with no
timeout arms nothing and there is nothing to cancel.
Summary
Functions
The event first, then the deadline.
A compound state whose one waiting child transitions to the received
outcome when the event arrives, and - when a timeout is stored - arms
a delayed send on entry and transitions to the timed_out outcome when
that send's event comes back.
One example event payload, so a palette panel can show what
_event.data looks like when the awaited event arrives.
core.wait's declaration byte for byte: a step that constrains nothing
and is constrained by nothing beyond being one.
The two ways an await can finish, for every config.
The event name, then the deadline when one is stored (ADR-0002 amendment H6).
Functions
The event first, then the deadline.
timeout's default is "" and not a duration: an await with no
deadline is the ordinary case, and a default that armed a timer would
be this type deciding an author's policy for them.
A compound state whose one waiting child transitions to the received
outcome when the event arrives, and - when a timeout is stored - arms
a delayed send on entry and transitions to the timed_out outcome when
that send's event comes back.
<state id="s_AWT" initial="s_AWT__waiting">
<state id="s_AWT__waiting">
<onentry><send delay="48h" event="statifier_blocks.await.blk_AWT" id="s_AWT__send"/></onentry>
<transition event="order.approved" target="s_AWT__o_received"/>
<transition event="statifier_blocks.await.blk_AWT" target="s_AWT__o_timed_out"/>
</state>
<final id="s_AWT__o_received">
<onentry><raise event="done.outcome.s_AWT.received"/></onentry>
</final>
<final id="s_AWT__o_timed_out">
<onentry><raise event="done.outcome.s_AWT.timed_out"/></onentry>
</final>
</state>The timer event carries the block id, so two awaits in the same chart
never wake each other, and the <send> names no target, which under
spec 6.2.2 is the running session's own external queue - the queue a
durable host turns into a durable timer. This type does not know
durable timers exist; it emits an ordinary delayed send.
With no timeout stored, the <onentry>, the timer transition and the
timed_out <final> are all absent, so an await with no deadline
compiles to the awaited transition and one final. The timed_out
outcome stays declared: a parent wiring
done.outcome.s_AWT.timed_out gets a transition that never fires
rather than an unresolved target (ADR-0004's outcome amendment, 2c).
What is annotated and what is not
The awaited event attribute is stamped as coming from the event
config key, so an upstream finding inside it reads as the author's typo
rather than a bug in this type (ADR-0004 decision 9). The delay
attribute is not, for the reason core.wait and core.send both give:
those bytes are not the author's verbatim, since a repeated unit
accumulates and a fraction expands.
One example event payload, so a palette panel can show what
_event.data looks like when the awaited event arrives.
Under statifier-ui's docs/fixture-bundles.md, events is one sample
_event.data payload per event name. The name here is an example, not
this block's configured event - fixtures/0 takes no config and
could not read one.
core.wait's declaration byte for byte: a step that constrains nothing
and is constrained by nothing beyond being one.
The two ways an await can finish, for every config.
See the moduledoc: the list does not follow timeout, because an
unreached outcome costs a parent nothing and a disappearing one costs
an author their wiring.
The event name, then the deadline when one is stored (ADR-0002 amendment H6).
The event comes first because it is what the block is for; the deadline
is only the other way out. Each half is dropped on its own when it is
absent or not well formed, so an await mid-edit shows the half the
author has filled in rather than nothing, and the stored bytes are
shown rather than the compiled ones for core.wait's reason - the card
reads back what the inspector field beside it holds.
iex> StatifierBlocks.Core.Await.summary(%{"event" => "order.approved"})
["order.approved"]
iex> StatifierBlocks.Core.Await.summary(%{"event" => "order.approved", "timeout" => "48h"})
["order.approved", "by 48h"]
iex> StatifierBlocks.Core.Await.summary(%{})
[]