core.subchart: a step that runs another chart and waits for it to
finish, routing on the outcome that chart finished with (ADR-0004's
2026-08-29 amendment, C1 through C3).
This type names a chart and an invoke type; it never runs either.
A subchart is not a new execution mechanism - it is a particular invoke
whose handler happens to start a child session, so it stands on the same
two-registry seam core.invoke does (ADR-0002 decision 2,
statifier-ex ADR-0051).
The child is a reference, never a body
A document is a tree and a chart is a build product of one, so a
subchart whose body lived inside the parent would be a second copy of a
document that already exists on its own, with its own id, its own
revision and its own runs. The child is therefore named by the
chart config field and this type declares no body slot at all.
How a child's outcome reaches the parent
Raised events are internal to the session that raises them, so
done.outcome.<state id>.<outcome> - the event ADR-0004's outcome
amendment gives a block - does not cross an <invoke>. What a parent
observes is the completion event and the data the child chose to send
with it (SCXML 3.7 and 5.5). C1 fixes the child half: a document
compiled for use as a child (StatifierBlocks.Compiler's
:child_use option) emits one top-level <final> per root-block
outcome, carrying <donedata><param name="outcome" expr="'<outcome>'"/>.
C2 fixes this half: the block's state routes done.invoke on
_event.data.outcome, one conditioned transition per declared outcome
and an unconditioned one last as the default path.
The condition is ===, not ==
Statifier's == against an absent _event.data.outcome is non-boolean
and raises a spurious error.execution beside the default arm's routing
(statifier-ex st-iz97); === is clean, and an explicit nil donedata
reads as null rather than as undefined. So the conditioned transitions
are written _event.data.outcome === '<outcome>'. The record fixes the
routing, not the operator; this is the campaign's recorded ruling on
which operator expresses it.
Which outcomes, and where the author says so
A block type cannot read the document it references - emit/2 is a pure
function of its block and its context, and the context deliberately
carries no palette and no other document. So the outcomes the referenced
chart declares are declared here, in the outcomes config field, one
name per line, in the order they should be routed. A subchart that
declares none has the one outcome an ordinary block has, done.
The failure outcome error is appended to whatever the author declared,
unless they declared it themselves - a child chart may well finish with
an outcome it calls error, and then the two are one outcome with one
final and one slot rather than two spellings of the same thing.
Every outcome gets a slot, on_error included
An outcome path is a slot, never a port (D13, ADR-0002's amendment
A2): every edge in a document is a parent/slot/child relationship, which
is the invariant the editor's rendered connectors rest on. So each
declared outcome gets one zero_or_one slot, on_<outcome>, holding
what runs when the child finishes that way, and on_error is exactly
core.invoke's slot with exactly core.invoke's :failure style - the
same declaration for the same concept, not a second wording of it.
A slot left empty is not a missing path: the routing transition simply
targets that outcome's <final> directly, and the block finishes there.
What it compiles to
<state id="s_blk_ELIG" initial="s_blk_ELIG__running">
<state id="s_blk_ELIG__running">
<invoke id="blk_ELIG" src="bdoc_CHILD" type="statifier_blocks:subchart"/>
<transition cond="_event.data.outcome === 'done'"
event="done.invoke" target="s_blk_ELIG__o_done"/>
<transition cond="_event.data.outcome === 'abandoned'"
event="done.invoke" target="s_blk_ELIG__o_abandoned"/>
<transition event="done.invoke" target="s_blk_ELIG__o_done"/>
<transition event="error.communication.invoke" target="s_blk_PARK"/>
</state>
...
</state><invoke> carries an explicit id (C3) so _event.invokeid is static
and a parent running subcharts in parallel can tell its concurrent
children apart by a value it knows at compile time. The id is the
block's own id, which ADR-0001 already guarantees is document-unique
and never reused, so nothing new has to be minted or kept unique.
Both done.invoke transitions and the error.communication.invoke one
match by SCXML's descriptor prefix rule and name no invocation, which is
safe for core.invoke's reason: they sit on the inner state, active only
while this block's own call is outstanding.
Summary
Functions
The outcomes the referenced chart declares, in the order the author
wrote them, defaulting to ["done"].
A compound state that runs the child chart in an inner state and
finishes at the <final> of whichever outcome the child reported.
The invoke type every core.subchart emits: the host-registered
child-chart invoke type.
core.invoke's io/1 exactly: a step with several outcomes, so
produces is :unknown rather than a join over the subtrees that reach
each one - the lattice ADR-0003 decision 4 refuses to build - and no
consumes, because a subchart reads its inputs through params.
The outcomes this block declares: the ones the author listed, then
error unless they listed it.
The outcomes field's rows: one outcome name per line, blank lines
ignored, in the order the author wrote them.
The outcomes the referenced chart declares, as the author listed them,
with error appended unless they listed it themselves (ADR-0002
amendment A1, ADR-0004's outcome amendment 2a).
One zero_or_one slot per declared outcome, named on_<outcome>, in
declaration order with on_error last.
Functions
@spec child_outcomes(StatifierBlocks.Block.config()) :: [String.t()]
The outcomes the referenced chart declares, in the order the author
wrote them, defaulting to ["done"].
Total: a field this type's validate_config/1 rejects reads as the
default rather than raising, so every callback stays answerable for a
config the compiler will refuse anyway.
A compound state that runs the child chart in an inner state and
finishes at the <final> of whichever outcome the child reported.
Every declared outcome gets one conditioned done.invoke transition, in
declaration order, and the unconditioned one comes last (C2):
document order decides which of several matching transitions is taken,
so an unconditioned transition placed anywhere but last would shadow
every conditioned one after it. Where it lands is this type's call, and
it lands on the first declared outcome - the outcome an author who
declared only one has, so a subchart that declares nothing behaves
exactly like a core.invoke.
error.communication.invoke routing is core.invoke's, unchanged: it is
emitted only when the on_error slot is occupied, and with the slot
empty the failure propagates as it does today.
Who owns what
Everything here is this block's except one transition per occupied slot:
the one leaving the slot's child for that outcome's final is attributed
to that child, because "what happens after the parking step
finishes" is a fact about the child (ADR-0004 decision 5, the rule
Emit.chain/2 follows). The src attribute's value is stamped as
coming from chart and the location's from assign_to, and each
condition as coming from outcomes, so an upstream finding inside one
is the author's typo rather than a bug in this type.
@spec invoke_type() :: String.t()
The invoke type every core.subchart emits: the host-registered
child-chart invoke type.
It is a constant rather than a config field because which handler
starts a child session is deployment state, not authoring state
(st-ADR-0051): an author picks a chart, and the host registers one
handler that knows how to run one. A host that registers nothing under
this name gets the ordinary two-registry lint
(StatifierBlocks.Compiler.InvokeTypes) rather than a runtime surprise,
which is the whole point of naming it here where the lint can see it.
core.invoke's io/1 exactly: a step with several outcomes, so
produces is :unknown rather than a join over the subtrees that reach
each one - the lattice ADR-0003 decision 4 refuses to build - and no
consumes, because a subchart reads its inputs through params.
@spec outcome_names(StatifierBlocks.Block.config()) :: [String.t()]
The outcomes this block declares: the ones the author listed, then
error unless they listed it.
Public because the editor and the tests both need the same reading of the flattened field, and two spellings of it would be two chances for them to disagree.
The outcomes field's rows: one outcome name per line, blank lines
ignored, in the order the author wrote them.
The outcomes the referenced chart declares, as the author listed them,
with error appended unless they listed it themselves (ADR-0002
amendment A1, ADR-0004's outcome amendment 2a).
One zero_or_one slot per declared outcome, named on_<outcome>, in
declaration order with on_error last.
Arity is zero_or_one for core.invoke's reason: an outcome path is
one continuation, not a list of them, and an author who wants several
steps there puts a core.sequence in it exactly as anywhere else a
single child is asked for.