StatifierBlocks.Core.Invoke (StatifierBlocks v0.27.0)

Copy Markdown View Source

core.invoke: a step that calls the host and waits for it to answer, with an optional subtree for the failure case (ADR-0002 decision 10, as amended 2026-08-29 section D).

This type names an invoke type; it never runs one. Which handler a name resolves to is deployment state supplied per session (st-ADR-0051), and the palette is authoring state supplied per operation - the two-registry seam ADR-0002 decision 2 draws, which this is the first core.* type to stand on. StatifierBlocks.Compiler.InvokeTypes is what a host compares the two against, at the one moment it holds both.

The failure path is a slot, not a port

A failing call is one of the two ways this block can finish, and the way an author says what happens then is by putting blocks in the on_error slot. It is not a second outlet with an author-drawn edge: every edge in a document is a parent/slot/child relationship, which is the invariant the editor's rendered connectors rest on (D13, and ADR-0002's amendment section A2). The slot is a rail beside the step, declared with zero_or_one arity and the :failure slot style ADR-0005 decision 10's 2026-08-29 amendment (10g) names for exactly this slot: an in-band continuation path taken on a bad outcome, rendered on the rail core.group's interrupts already uses but in the error family rather than the interrupt one. The rendering half of 10h is the renderer's, not this type's: all a block type does is declare the style.

Two outcomes

done and error, in that order. Under ADR-0004's outcome amendment each one the block reaches compiles to its own <final>, minted through StatifierBlocks.Compiler.Context.outcome_id/2, whose entry raises done.outcome.<state id>.<outcome>. A parent that does not care which way the call went wires the prefix and never learns an outcome name; one that does names the full event. The block decides nothing about what comes next: its emission ends at the final it enters.

assign_to names a datamodel path

The field is declared {:path, %{}} - ADR-0002 decision 7's eighth field type - so the editor offers the host's declared datamodel paths as candidates on it, a value the datamodel does not declare draws ADR-0005 clause 11e's :info advisory rather than a refusal, and the write this block makes is visible to StatifierBlocks.Environment as :unknown at that path (ADR-0011 decision 2) instead of being emitted where nothing declared can see it. Its rule is core.assign's and core.subchart's - any non-empty path with no whitespace - for ADR-0011 decision 13's reason: one <assign> element writing one datamodel cannot have two rules about what a location may be. A blank assign_to is a call whose answer is thrown away, which is an answer rather than a gap.

The params field is knowingly provisional

ADR-0002 decision 7's field types are a closed set and none of them is "a list of name/path pairs", so params is a :string holding one name=path per line. That is the amendment's own compromise, recorded in its section D1 as a deferred question: whichever way it is resolved - a new field type, a dedicated control, or the flattening as shipped - it is a decision 7 change rather than a change to this type's row.

Summary

Functions

A compound state that runs the call in an inner state and finishes at the <final> of whichever outcome it reached.

error is failure-classed: a call that came back on the error path did not succeed (ADR-0002's amendment of 2026-09-06, section 3).

A step with two outcomes, so produces is :unknown for the reason core.branch declares it: joining the type this block's own call produces with the type its on_error subtree produces is the lattice ADR-0003 decision 4 refuses to build.

done and error, in that order - the pair this type has always emitted (ADR-0002's amendment of 2026-09-06, section 1).

The params field's rows, as {name, path} pairs in the order the author wrote them.

One optional slot, on_error, holding what runs when the call fails.

Functions

emit(block, context)

A compound state that runs the call in an inner state and finishes at the <final> of whichever outcome it reached.

<state id="s_INV" initial="s_INV__running">
  <state id="s_INV__running">
    <invoke type="myapp:authorize">
      <param expr="order.amount" name="amount"/>
    </invoke>
    <transition event="done.invoke" target="s_INV__o_done">
      <assign expr="_event.data" location="authorization"/>
    </transition>
    <transition event="error.communication.invoke" target="s_blk_PARK"/>
  </state>
  {the on_error child's own subtree}
  <transition event="done.state.s_blk_PARK" target="s_INV__o_error" type="internal"/>
  <final id="s_INV__o_done">
    <onentry><raise event="done.outcome.s_INV.done"/></onentry>
  </final>
  <final id="s_INV__o_error">
    <onentry><raise event="done.outcome.s_INV.error"/></onentry>
  </final>
</state>

Both transitions match by prefix, and neither names an id

The <invoke> carries no id, so the engine mints one, and the two transitions match done.invoke and error.communication.invoke by SCXML's descriptor prefix rule rather than naming the invocation. That is safe for exactly the reason ADR-0004's worked example gives: both transitions sit on the inner state, which is active only while this block's own call is outstanding, so no other invocation's completion can be selected by them.

error.communication.invoke.<invoke_id> is statifier-ex ADR-0068's name - a blessed suffix extension of the error.communication that st-ADR-0051 decision 1 already assigns to this failure - which is what makes the prefix match here and a host chart's existing error.communication handler both correct at once.

An absent on_error still ends the block on error

ADR-0002's amendment of 2026-09-06, section 2: the failure final is emitted whether or not the slot is occupied, because a class is read off a final - the only thing that raises done.outcome.<state id>.error is that final's own onentry, and an outcome whose final is sometimes absent cannot be classed. With the slot empty the failure transition targets the final directly; with it occupied every byte is what it was, the transition targeting the child and the child's own done_event carrying it into the final.

An author who wants the old silence for a particular call has the slot: an occupied on_error is the amendment's section 4 definition of handling, and a handled failure reaches nothing above the block.

Who owns what

Everything here is this block's except one transition: the one leaving the on_error child for the error final is attributed to that child, because "what happens after the parking step finishes" is a fact about the child (decision 5, the same rule Emit.chain/2 follows). The type attribute's value is stamped as coming from invoke_type and the location's from assign_to, so an upstream finding inside either is the author's typo rather than a bug in this type.

failure_outcomes(config)

error is failure-classed: a call that came back on the error path did not succeed (ADR-0002's amendment of 2026-09-06, section 3).

That is what the outcome has always meant and what the :failure slot_style on on_error has always drawn. The on_error slot is unchanged by the class - amendment section A2's refusal to marry an outcome to a slot stands, and the slot and the outcome share a word here the way core.subchart and core.map have had both for longer.

io(config)

A step with two outcomes, so produces is :unknown for the reason core.branch declares it: joining the type this block's own call produces with the type its on_error subtree produces is the lattice ADR-0003 decision 4 refuses to build.

consumes is absent - an invoke reads its inputs through params, out of the datamodel, rather than through the type flow.

outcomes(config)

done and error, in that order - the pair this type has always emitted (ADR-0002's amendment of 2026-09-06, section 1).

Fixed rather than config-derived, and the same pair StatifierBlocks.InvokeStep.outcomes/0 returns for every host type built on it. Until that amendment this type exported no outcomes/1 at all, so StatifierBlocks.BlockType.outcomes/2 answered the default single done while emit/2 minted an error outcome final beside it - a raised event in no summary, which ADR-0004's outcome amendment forbids in its clause 2e.

Declaration order is read by ADR-0004 decision 6's byte determinism, so this list is never sorted.

param_rows(value)

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

The params field's rows, as {name, path} pairs in the order the author wrote them.

One name=path per line, blank lines ignored. 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 field is provisional (ADR-0002's amendment, D1) and this function is where the provisionality is contained.

slots(config)

One optional slot, on_error, holding what runs when the call fails.

Its arity is zero_or_one because a failure path is one continuation, not a list of them: an author who wants several steps there puts a core.sequence in it, exactly as anywhere else a single child is asked for.