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

`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
(pinned upstream in statifier-ex); `===` 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.

## What the host knows that this type cannot

The paragraph above is a statement about what a *compile of one
document* can see. A **host** sees more: it holds every stored document,
so it knows which of them it compiles with `:child_use` and what finals
each of those emits. Two things follow, and both are the editor's rather
than this type's.

  * The editor offers those finals as candidates on the `outcomes`
    field, keyed on the document id in `chart`, from the host's
    `chart_outcomes` assign. It is a `<datalist>` on a field that is
    still a `:string`: a free-typed name validates exactly as it did.
  * `StatifierBlocks.ViewModel.outcome_findings/3` reports a
    **disagreement** between what the author declared here and what the
    host says that chart finishes with, anchored on this key.

The comparison is against `child_outcomes/1` - the author's own list -
and not `outcome_names/1`, because the appended failure outcome is
ADR-0068's event rather than a `<final>` the child reports, and a child
chart is not expected to have one. A ref the host said nothing about
produces nothing: *unknown is not disagreement*, which is ADR-0005
amendment `11f`'s posture for the datamodel repeated here for the same
reason.

None of it constrains. The disagreement is a `:warning`, not an error:
the document compiles either way, and what a mismatch actually costs is
a conditioned transition that can never match - a routing arm that is
dead rather than wrong. `validate_config/1` is untouched, because this
type still cannot read the chart it names.

## Every outcome gets a slot, `on_error` included

An outcome path is a **slot**, never a port (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.

## Where the outcome is written

`assign_to` names a location in the host's datamodel, so it is declared
a `{:path, %{}}` field - ADR-0002 decision 7's eighth field type, added
by its 2026-09-05 amendment, which names this field as the one core
field that held a path and declared nothing about it. Two things follow
and a third deliberately does not:

  * the editor offers the declared datamodel paths as candidates, drawn
    from `StatifierBlocks.Datamodel.candidates/3`; and
  * a value the datamodel does not declare gets ADR-0005 clause 11e's
    `:info` advisory anchored on the `assign_to` key, which is a remark
    and not a refusal.

**The field accepts a datamodel path** (ADR-0011 decision 13): any
non-empty string with no whitespace in it, which is exactly what
`core.assign` accepts for the path it writes, read out of one
internal datamodel-path predicate. `validate_config/1` and
`emit/2` are widened together, because the emission has to answer for a
config the validation would have rejected.

It is a widening and nothing else: a bare identifier is a one-segment
path, so every document written before this type existed keeps
validating and keeps compiling to the same bytes, and `emit/2` still
writes the author's string verbatim into the `<assign>`'s `location`.
What it settles is that the field no longer refuses the dotted paths its
own candidate list offers - a control that offers what its validation
refuses is a defect either way round. The identical refusal on an
`<assign>` location elsewhere in the vocabulary is untouched: that
decision was ruled about this field, and widening the others on the
strength of one field's argument is a sweep it did not make.

## What `src` names, and what it does not

The emitted `src` is the **document id** the author typed into `chart`,
and nothing else (ADR-0004's subchart-src amendment). It is *not*
statifier-ex ADR-0052 chart identity: that identity is a hash of emitted
bytes, so it moves every time the child is republished and cannot be
known when the parent is authored. A document id is the stable
authoring-time reference, and the host's handler registered under
`statifier_blocks:subchart` (st-ADR-0051) resolves it to whichever
chart the host currently publishes for that document. Pinning a
*particular* child revision at publish time is a host provenance
concern, carried in run metadata; the compiler does not do it.

One thing a compile of one document can decide about that id, and it
does: a subchart may not name the document it sits in. See
`StatifierBlocks.Compiler.SelfReference`, which also says why a cycle
through two or more documents is the host resolver's to refuse.

# `child_outcomes`

```elixir
@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.

# `emit`

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: the
transition is always emitted, targeting the `on_error` child when the
slot holds one and the `error` final directly when it does not
(ADR-0002's amendment of 2026-09-06, section 2). That final is emitted
in both cases, whatever the referenced chart's own declared outcomes
say, because a class is read off a final.

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

# `failure_outcomes`

`error` is failure-classed: a child that reported `error` is a child
that finished badly (the failure seam, 2026-09-06).

It is the one outcome this type appends itself, and the moduledoc
already calls it "the failure outcome"; nothing else the author listed
is classed, because this package cannot know what a chart's own
`declined` or `expired` means. The class is a second axis on an outcome
that already exists, so the outcome list, the `on_<outcome>` slots and
the `error.communication.invoke` routing are all unchanged - and so is
the `slot_style` `:failure` the palette entry already gives `on_error`,
which is the editor's word for the same fact.

It changes one thing, and only for a document whose **root** block is a
`core.subchart`: the top-level `<final>` for `error` carries the
reserved `<donedata>` `<param>` that tells a durable stepper the run
failed.

# `invoke_type`

```elixir
@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.

# `io`

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

# `outcome_names`

```elixir
@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.

# `outcome_rows`

```elixir
@spec outcome_rows(term()) :: {:ok, [String.t()]} | {:error, String.t()}
```

The `outcomes` field's rows: one outcome name per line, blank lines
ignored, in the order the author wrote them.

# `outcomes`

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

# `sentence`

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

The chart it runs and the fact that it waits, which are the two things
that separate this block from every other step in a list. The chart name
is repeated as the author wrote it - it is a key into the host's chart
store, and a line that paraphrases it names nothing the author can look
up.

A block with no chart named yet says what it is and stops.

    iex> StatifierBlocks.Core.Subchart.sentence(%{"chart" => "signup.identity"})
    "Run signup.identity and wait"

    iex> StatifierBlocks.Core.Subchart.sentence(%{})
    "Run a chart and wait"

# `slots`

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.

---

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