# `StatifierBlocks.Core.Await`
[🔗](https://github.com/riddler/statifier_blocks/blob/v0.44.1/lib/statifier_blocks/core/await.ex#L1)

`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 optional `capture` map, which declares no field, copies values
out of the awaited event's payload into the datamodel.

## The optional `capture` map

`capture` is `core.on_event`'s map, with its semantics and its key
spelling: the key is a datamodel path written, the value is a path
inside `_event.data` read or a `["const", value]` literal, a path the
payload does not carry writes nothing, and the pairs are written in
their datamodel paths' sorted order. `StatifierBlocks.Core.OnEvent`'s
moduledoc records the rules, and one shared implementation validates
and emits the map for both types, so the two cannot drift apart. The
editor draws the same capture rows for both.

The assigns ride the transition the awaited event selects, inside it,
because `_event.data` is in scope for that transition and for nothing
after it: a `core.assign` placed after the await runs in a later
microstep with a different `_event`, and the payload is gone. The
deadline's transition carries none - its event is this block's own
timer and carries no payload of the host's.

An await whose `capture` is absent or empty compiles to exactly the
bytes it compiled to before the key existed, so the key is additive
and `current_version/0` stays 1. `core.on_event`'s `payload`
declaration is not shared: it adds a compile-time refusal of a read
past a declared payload, and nothing about what a capture writes
depends on it. See ADR-0002's Note of 2026-10-19.

## 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.

# `config_schema`

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.

# `emit`

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).

## A capturing await

Each `capture` pair becomes the `<assign>` `core.on_event` writes for
it, as a child of the awaited transition; a path pair's assign sits
inside an `<if>` that tests the path for presence, and a literal pair's
is bare:

    <transition event="loan.requested" target="s_AWT__o_received">
      <if cond="_event.data.copy_id !== undefined">
        <assign expr="_event.data.copy_id" location="loan.copy_id"/>
      </if>
      <assign expr="&quot;desk&quot;" location="loan.source"/>
    </transition>

Nothing else in the shape moves, and an await with no `capture` writes
the self-closing transition sketched above.

## 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.

# `fixtures`

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.

# `io`

`core.wait`'s declaration byte for byte: a step that constrains nothing
and is constrained by nothing beyond being one.

# `outcomes`

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.

# `sentence`

This block as one line of prose (ADR-0002's 2026-09-07 amendment).

The event it holds for, then the deadline when one is stored, in the
words the config form labels the deadline with. Each half is held to
the same test `summary/1` holds it to, so a half that is absent or not
well formed is left out of a line that reads as settled rather than
repeated from the field: an await with no usable event says it waits for
"an event", and one with no usable deadline says nothing about giving
up, because no timer is armed.

    iex> StatifierBlocks.Core.Await.sentence(%{"event" => "copy.returned", "timeout" => "14d"})
    "Wait for copy.returned, giving up after 14d"

    iex> StatifierBlocks.Core.Await.sentence(%{"event" => "email.verified"})
    "Wait for email.verified"

    iex> StatifierBlocks.Core.Await.sentence(%{})
    "Wait for an event"

# `summary`

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(%{})
    []

---

*Consult [api-reference.md](api-reference.md) for complete listing*
