core.send: a leaf step that names an event and says when.
PROVISIONAL - not yet in the vocabulary table
core.send is a campaign-013 spike proposal
(spike/js/proposed-core.js) that ADR-0002 decision 10's vocabulary
table, and its 2026-08-28 amendment, do not yet carry - the
amendment's section D promotes core.invoke and core.raise only.
This type is shipped under the campaign's filing; the decision-10 row
it is owed has not been written.
Two config fields: event, in the same grammar core.raise and
core.on_event speak, and delay, an optional duration.
Why this is not core.raise with a delay key
core.raise names an event and hands control on, now: the event goes on
the internal queue and is processed before anything else the session is
holding, which is the property an enclosing group's interrupt rail
depends on. A delayed send goes on the external queue, outlives the
step that armed it, has to survive a restart, and is turned into a
durable timer by infrastructure outside the interpreter (statifier-ex
docs/durable-timers.md, ADR-0054/0059; the sob- lane). A delay key
that quietly turned a raise into a durable timer would make
core.raise's whole note false half the time, so they are two types.
The block finishes when the send is armed
This is what separates core.send from core.wait. A wait keeps the
chart live for the duration; a send arms an event and completes in the
same macrostep. So there is still one outcome and still no join to
refuse - io/1 leaves produces absent rather than :unknown, the way
core.raise and core.wait do.
Two things it deliberately does not declare
No target. What a target may name is not this repo's decision:
session identity is statifier-ex's (ADR-0052) and reaching the host is
the invoke seam st-ADR-0051 defines, which this vocabulary already spells
core.invoke. A config key that validates nothing is a proposal made by
accident. Absent a target, spec 6.2.2 puts the event on the running
session's own external queue, which is exactly what statifier_oban
turns into a durable timer.
No cancel - and that is a recorded gap, not an oversight. A delayed
send this block arms is never cancelled by anything this package emits:
no <cancel>, and no sendid an author could name. A cancel names the
send it cancels, which makes it a cross-subtree reference to another
block - the exact shape ADR-0005 decision 13 refused - and the
alternative that keeps the tree invariant is scope-shaped rather than
reference-shaped: a delayed send is cancelled when the region that armed
it is left. Which of those is right is the delayed-send lifetime ruling,
filed here as sb-b4f and mirrored to statifier-ex as st-q3ud. It
is out of scope for campaign 014 and nothing here should grow a cancel
until it is ruled. The consequence a reader needs today: an armed send
fires even if the block, its group, or the whole chart moved on.
The delay's stored form
delay is the vocabulary's first optional duration, and "no delay" is
neither PT0S nor an unfinished field - an absent or empty delay emits
no delay attribute at all, so the event goes out now.
A present delay may be stored in either spelling: ISO-8601
(PT2H), or a predicator duration string (1h30m, 2d, 3d8h). That
two-spelling stored form is campaign 014's D4 proposal, filed as
sb-709 and implemented in the spike's duration control; it is a
recorded proposal, not a ruling, and ADR-0002 decision 7's
:duration field type is unchanged. StatifierBlocks.Core.Duration is
where the compile lives and where the grammar's one home - predicator's
own lexer - is read rather than mirrored.
Summary
Functions
The event first, then the delay.
A compound state whose entry sends the event and immediately goes final.
core.raise's io/1 exactly, and for core.raise's reason: one
outcome, so produces is absent rather than :unknown. A send reads
nothing through the type flow - its event name and its delay are its
own config - so there is no consumes either.
The spike's descriptor declares a static sends badge here. ADR-0005
decision 10's palette_entry/0 key set has no badge, so there is
nothing to declare it in yet and the icon carries the whole visual
signal - a Phase-B gap the spike's own note already records.
Functions
The event first, then the delay.
The delay's default is "" and not a duration: a send with no delay is
the ordinary case, and core.wait's PT1H default would author a timer
nobody asked for.
A compound state whose entry sends the event and immediately goes final.
<state id="s_blk_SND" initial="s_blk_SND__done">
<onentry><send delay="2h" event="signup.abandoned"/></onentry>
<final id="s_blk_SND__done"/>
</state>There is nothing to sequence - arming the send is instantaneous and the
block is done the moment it is entered - so initial points straight at
the <final> and no auxiliary state is minted. That is core.raise's
shape with a different element inside the <onentry>, which is the
right way round: the difference between the two types is which queue the
event lands on, not how long the block lasts.
With no delay the <send> carries no delay attribute at all, so the
event goes out on the current macrostep. Emission.element/3 drops a
nil attribute, which is how "no delay" and delay="" stay
indistinguishable in the emitted bytes.
event is attributed and delay is not
ADR-0004 decision 9's annotation is for an attribute whose value is the
author's verbatim. event is: those bytes came out of the config
unchanged. The delay attribute's bytes did not - a stored PT2H
emits as 2h, and a stored 1h30m is canonicalised through ISO-8601
before it is rendered - so annotating it would point a runtime finding
at a span the author never typed. core.wait leaves its own delay
unannotated for the same reason.
No <cancel>, and no id to cancel by
The <send> is deliberately anonymous: minting a sendid here would be
half of a cancel mechanism whose other half sb-b4f (mirrored as
statifier-ex st-q3ud) has not ruled on, and a half-built one is worse
than none. See the moduledoc.
core.raise's io/1 exactly, and for core.raise's reason: one
outcome, so produces is absent rather than :unknown. A send reads
nothing through the type flow - its event name and its delay are its
own config - so there is no consumes either.
The spike's descriptor declares a static sends badge here. ADR-0005
decision 10's palette_entry/0 key set has no badge, so there is
nothing to declare it in yet and the icon carries the whole visual
signal - a Phase-B gap the spike's own note already records.