This page is for a host: the application that compiles charts, starts
executions and supplies the services a chart reaches through <invoke> and
<send>. For each release from 2.6.0 to 2.13.0 it says what a host must
change to take the release, and then what a host may start doing with it.
Where a host must change nothing, the page says NONE.
The Changelog says what the library changed; this page
does not repeat it.
No release in this range has a Breaking entry in the CHANGELOG. Every
"must change" from 2.6.0 to 2.10.0 is NONE. 2.11.0 changes some answers,
and its "must change" names the hosts each one reaches: a host whose
charts carry a <send> <param> with both expr and location, a host
that matched an error shape or rescued a raise the release replaces with a
named refusal, and a receiver that parsed a list or map parameter's text.
2.12.0 changes some answers too, and its "must change" names the hosts
each one reaches: a receiver of a Basic HTTP send whose <content expr>
evaluates to a struct, a host whose charts carry an initial on a
compound state with no id naming a target outside that state, a host
that reads the entries of a document already refused for a shared id,
a host that holds predicator at 9.0 to 9.3, and a chart with a \u or
\U escape in a string literal. 2.12.1 changes documentation only, and
its "must change" is NONE. 2.13.0 changes some answers, and its "must
change" names the hosts each one reaches: a host whose charts carry an
initial on a <state> with no id that names a missing state or sits
on a state with no children, a host that reads the entries of a document
already refused for a shared id, and a host that waited for a further
halted message after reporting a budget-halted session's failed send.
A dependency requirement of {:statifier, "~> 2.5"} already accepts every version on this page. Raise it only when
your host calls a function a later release added: each section names the
lowest requirement its functions need.
2.6.0
A host must change: NONE. A session started without :send_types
behaves as it did in 2.5: a <send> whose type is not built in still
raises error.execution.
A host may start: handing <send> types to its own processors
(ADR-0069). Requires {:statifier, "~> 2.6"}.
- Write a module per type that implements
Statifier.Send.Processor:deliver/3andcancel/2are required and pure; the optionalperform/2does the delivery, and the optionalioprocessors_entry/1fills the type's_ioprocessorsentry.perform/2may be called more than once for the same send, so make it idempotent. A delayed send is your processor's timer: the session schedules nothing for it. - Pass the map to
Statifier.Session.start_link/2assend_types: %{"my-type" => MyProcessor}, and addinherit_send_types: trueif the children a chart starts through<invoke>should get the same map. A map that names a built-in type ("scxml", the SCXML Event I/O Processor URI, ornil) makes the session refuse to start with{:error, {:send_types, {:built_in_types, types}}}. - When your processor cannot deliver a send and the sender still exists,
call
Statifier.Session.failed_send/3; the chart then seeserror.communicationwith the send's id. When the sender has finished, that call writes nothing and recording the miss is yours. A host without a session process makes the same write withStatifier.Interpreter.deliver_internal/5. - At publish time, refuse a chart that names a type you never registered:
Statifier.Send.Types.unsupported_sends/2, called with the compiled chart andStatifier.Send.Types.from_send_types/1of the map you will start it with, lists each such<send>with its location. - A resumed session does not remember which processor holds a delayed
send. Routing a
<cancel>for a send handed over before the position was saved is yours, as re-arming timers after a resume already is.
The guide is the "<send> half" of
How to extend Statifier: <invoke> handlers and <send> types.
2.6.1
A host must change: NONE. The release adds conformance cases under
conformance/, which is not in the Hex package.
A host may start: nothing new. A sibling implementation that vendors
the corpus re-vendors it at the v2.6.1 tag; that is not a host step.
2.7.0
A host must change: NONE.
A host may start: checking a chart's events at publish time
(ADR-0071). Requires {:statifier, "~> 2.7"}.
Statifier.Chart.events/1gives a compiled chart's event vocabulary: every event descriptor on a transition from a state some path can enter, as authored.Statifier.Chart.check_accepts/2compares a declared list of the event names the chart accepts with that vocabulary and answers%{unreachable: [...], undeclared: [...]}.unreachableholds the declared names the chart can never select on;undeclaredholds the descriptors the declaration leaves out. Withnilfor the declaration both lists are empty. To ask whether one event name reaches the chart, callcheck_accepts(machine, [name])and readunreachable.
Both functions report and refuse nothing: which list refuses a publish is your decision. Publish-time checks: every runtime refusal and its twin lists the runtime refusal each one stands in front of.
2.8.0
A host must change: NONE.
A host may start: comparing two compiled charts before publishing the
second, and checking whether one execution's position survives the edit
(ADR-0072). Requires {:statifier, "~> 2.8"}.
Statifier.Chart.diff/3takes the published chart, the candidate and an optionalmapping:of renamed state ids (old id to new id), and answers%{class: class, reasons: reasons}with a class of:identical,:compatible,:mappedor:breaking. It raisesArgumentErroron an option other thanmapping:or an invalid mapping.Statifier.Position.compatible_at?/3takes the two charts and one execution'sStatifier.Position.export/1map and answerstrueonly when that execution's position is untouched by the edit. Both charts must carry their source, whichStatifier.compile/2keeps; a chart built without it answersfalse.
Neither function moves an execution, and the library calls neither. Whether an execution moves to the new chart, and when, stays your decision.
2.8.1
A host must change: NONE.
An <invoke> whose type is http://www.w3.org/TR/scxml, the SCXML type
URI without its trailing slash, now starts an SCXML child session; before
2.8.1 it raised error.execution. A handler your host registered under
:invoke_handlers for that exact string still receives it, since a
registered handler takes precedence over the built-in one. If a chart of
yours carries that spelling and your host relied on the
error.execution, that chart now starts a child instead. A host that
needs the new behaviour requires {:statifier, "~> 2.8 and >= 2.8.1"}.
A host may start: nothing new. The corpus changes follow 2.6.1's
rule: a sibling implementation re-vendors at the v2.8.1 tag.
2.9.0
A host must change: NONE. Statifier.MachineState gains a
last_selection field, which a position blob does not carry, so a
position saved under 2.8.1 restores as before.
A host may start: running every publish-time check in one call.
Requires {:statifier, "~> 2.9"}.
Statifier.Publish.findings/2takes a compiled chart and a declaration of the host'ssend_types:,invoke_types:andaccepts:, and answers a list of findings, each naming its row of Publish-time checks: every runtime refusal and its twin with akind, a location and its data. Like 2.7.0's functions, it refuses nothing: which finding refuses a publish is your decision.Statifier.MachineState'slast_selectionreads:selectedwhen the last external event selected a transition,:nonewhen it selected none, andnilbefore any external event.
2.10.0
A host must change: NONE. A session that registers no send type sees
nothing new: no request is made, nothing is started, and _ioprocessors
holds the SCXML processor's entry alone. A {:statifier, "~> 2.9"}
requirement already accepts 2.10.0.
A host may start: the W3C Basic HTTP Event I/O Processor (ADR-0075).
Requires {:statifier, "~> 2.10"}.
- Register
{Statifier.Send.BasicHTTP, base_url: base}in:send_typesunder both of its type strings,"http://www.w3.org/TR/scxml/#BasicHTTPEventProcessor"and"basichttp". Each string's_ioprocessorsentry holds one"location": the base URL,/, and the session's_sessionid. A registration without:base_urlrefuses the session's start with anArgumentError. The POSTs go through OTP's:httpcby default, and a:transportoption names your ownStatifier.Send.BasicHTTP.Transport; the package adds no dependency. - The processor receives nothing on its own: your front answers at the
base URL and hands each request to
Statifier.Send.BasicHTTP.decode/1, which answers the event to enqueue or the reason to refuse the request. - Every POST carries the send's deduplication key in an
scxml-send-keyheader. A front that enqueues a request only when it has not already enqueued one with the same value delivers each send once; one that ignores the header sees at-least-once delivery. - For a processor of your own, a
:send_typesvalue may now be{module, opts}: the options reachdeliver/3andcancel/2under the plan context's:optskey. A processor may implement the optionalioprocessors_entry/2, which receives the type string and a context carrying the session id and the options, so its entry can address one session. A bare-module registration behaves as it did in 2.9.0.
The guide is The Basic HTTP Event I/O Processor. The
conformance corpus claims the processor's W3C documents and its case
schema gains host.event_io_processors; a sibling implementation
re-vendors the corpus at the v2.10.0 tag, which is not a host step.
2.11.0
A host must change: only where one of these reaches it. A session that
registers no send type sees nothing new, and a {:statifier, "~> 2.10"}
requirement already accepts 2.11.0.
- A chart whose
<send>carries a<param>with bothexprandlocationno longer compiles:Statifier.Validator.validate/2, and soStatifier.compile/2, refuses it as{:param_expr_and_location, name}. Before 2.11.0 it compiled and the parameter tooklocation. Drop one of the two attributes; keepinglocationkeeps what the chart sent. A<send><param>with neither attribute is refused as{:param_no_value, name}, whereStatifier.compile/2raisedFunctionClauseError. - A fresh
Statifier.Session.start_link/2whoseStatifier.Send.BasicHTTPregistration has no usable string:base_urlanswers{:error, {:send_types, {:invalid_registration, type, {:missing_option, :base_url}}}}, refused before any session process is spawned and so with no crash report. It answered{:error, {%ArgumentError{}, stacktrace}}. If your host matched theArgumentErrorshape, match the named refusal instead. Every registration the processor can build its entry from still starts. A resume is not refused and does not change: its position carries the_ioprocessorsentries it started with.ioprocessors_entry/2still raisesArgumentErrorfor a direct caller. Corrected 2026-10-04: the named refusal is decided before the session process starts, so a start that also had another fault answers the named refusal too, where the start answered that other fault: a:namealready registered ({:already_started, pid}), an:invoke_handlersvalue that is not a map ({:function_clause, stacktrace}), or another processor's_ioprocessorsentry that is not a map (that processor'sArgumentError). Beside another processor whosecheck_registration/2breaks its contract (it raises, throws, exits or answers anything other than:okor{:error, reason}), the registration is not refused by name and the start answers theArgumentErrorshape as before. ADR-0069's Note of 2026-10-04 records this. Statifier.Lowering.lower/2, and soStatifier.compile/2, answers{:error, [%Statifier.Lowering.Error{reason: {:unexpected_root, name}}]}for a document whose root is any SCXML element other than<scxml>(a<state>, a<parallel>, a<final>, a<history>, a<transition>, ...), where it raisedBadMapError, and{:unsupported_element, "scxml"}for an<scxml>nested below the root, where it raisedFunctionClauseError. If your host rescued either raise, match the error tuple instead.Statifier.Validator.validate/2no longer raisesFunctionClauseErroron a<history>whose compound parent has noid: a default target outside the parent is reported as{:initial_not_descendant, target, nil}. If your host matches that reason's parent id as a binary, handleniltoo.Statifier.Send.BasicHTTPwrites a parameter value that is a list or a map, and a<content>body that is a list, as JSON text (["Dune",2],{"title":"Dune"}), where it wrote the value'sinspect/1text; an undefined value inside such a list or map is written as JSONnull. A list or map holding anything with no JSON form (a key that is not a string, a struct such as aDate, a tuple, a string that is not UTF-8) keeps its wholeinspect/1text, and every scalar keeps its text. A<content>body that is a list is still sent astext/plain. If a receiver of your POSTs parsed theinspect/1text, read JSON instead. The inbound decoder is unchanged.
Three changes to a Statifier.Send.BasicHTTP delayed send ask nothing of a
host, but a chart sees them. A delayed send whose transport raises now
reaches the sender as error.communication, through
Statifier.Session.failed_send/3 with the reason {:raised, exception};
before, nothing reached the chart. A delayed send of a live session that is
busy when the delay passes is now POSTed; before, it was dropped. A cancel
the send's timer has received before its POST now always wins. An
immediate send whose transport raises is unchanged.
A host may start: refreshing a session's registered _ioprocessors
entries, refusing a registration in its own processor, and telling a
processor's own process that its session halted. Requires
{:statifier, "~> 2.11"}.
Statifier.MachineState.refresh_ioprocessors/1recomputes a position's registered_ioprocessorsentries from the registration it is stamped with, and answers{:ok, machine_state}or a registration's{:error, reason}with nothing changed; an entry that raises raises here. A host whose Basic HTTP base URL moved across a resume re-stamps the position withStatifier.MachineState.put_send_types/2, refreshes it, and then passes it as:resume.Statifier.Session.refresh_ioprocessors/1refreshes a live session and answers:ok, or, changing nothing,{:error, reason}for a registration its processor rejects ({:error, {:missing_option, :base_url}}for Basic HTTP without:base_url),{:error, :not_running}once the session has halted,{:error, :recorded_session}for a session started withrecord: true, or{:error, {:ioprocessors_entry, type, exception}}when an entry raises, the session running on at the position it held. The SCXML entry and an entry from a processor exporting onlyioprocessors_entry/1are untouched, and a resume still reads the entries its position carries. A session that registers nothing answers success and sees nothing change. ADR-0075's Amendments of 2026-10-02 record both calls.- A processor of yours may implement the optional
Statifier.Send.Processorcallbackcheck_registration/2, which answers:okor{:error, reason}for a registration's type string and options. A fresh start asks it for every registration whose module exports it and refuses the first one rejected with{:error, {:send_types, {:invalid_registration, type, reason}}}, before any session process is spawned; a resume does not ask it. A processor that does not implement it is not asked. ADR-0069's Amendment of 2026-10-02 records the callback. Corrected 2026-10-04:Statifier.Send.BasicHTTPimplements the callback, so 2.11.0 also adds the public functionStatifier.Send.BasicHTTP.check_registration/2, which a host may call to check a Basic HTTP registration's options before a start. It answers:okexactly whenioprocessors_entry/2would build an entry from the options (a string:base_url), and{:error, {:missing_option, :base_url}}for any other registration, a:base_urlthat is present but not a string included. - A processor that holds a delay in a process it starts from
perform/2can hand that process toStatifier.Session.HaltNotice.watch/2; the session sends it{:statifier_halted, session, reason}when it halts (:done,:cancelledor:budget_exhausted), at once when it already has.HaltNotice.take/1removes and returns the processes watched under a key. Outside a sessionwatch/2keeps nothing and answers:not_a_session. The delay stays the processor's (ADR-0069 decision 4).
The conformance corpus changes too. The W3C Basic HTTP documents test518,
test519, test520 and test534 now check the decoded event their transition
takes, not its name alone, and a statifier case's step may carry an
optional expect_position, the position the chart holds after that step
(ADR-0076), with three new library cases. The corpus hash in
conformance/manifest.json moves, so a sibling implementation that
vendors the corpus re-vendors it at the v2.11.0 tag, which is not a host
step.
2.12.0
A host must change: only where one of these reaches it.
Statifier.Send.BasicHTTPsends a<content expr>that evaluates to a struct as the body, itsinspect/1text astext/plain. Before 2.12.0 such a content took the form arm. A struct that does not enumerate as parameter pairs (aDate, for example) made planning the send raise (Protocol.UndefinedErrorfor aDate), the session performing it exited, and no request was made. A struct that does enumerate as pairs (aMapSetof two-element tuples, for example) was sent as anapplication/x-www-form-urlencodedbody of those pairs; a receiver of such a send now gets the struct'sinspect/1text astext/plaininstead, the text it already reads for a struct inside a list or a map. A session that registers no send type sees nothing new.Statifier.Validator.validate/2, and soStatifier.compile/2, refuses a chart whose compound<state>has noidand carries aninitialattribute or an<initial>element naming a target outside that state: it reports{:initial_not_descendant, target, nil}. Before 2.12.0 such a chart validated andStatifier.compile/2raisedKeyError. A target inside the state is accepted as before. If your host rescued the raise, match the refusal instead, and if it matches that reason's parent id as a binary, handleniltoo, as 2.11.0 asked for a<history>parent.Statifier.Validator.validate/2, and soStatifier.compile/2, decides whether a<history>default target sits under the history's parent by the document's structure for every parent, as it already did for a parent with noid. A document where two states share anidand a history under one of them names a target under the other was already refused for thatid({:duplicate_id, id}, or{:empty_id}for an empty one); it now also reports{:initial_not_descendant, target, parent_id}. No document that validated before is refused, and none is accepted that was refused.statifierrequires{:predicator, "~> 9.4"}, where it required~> 9.0. A host that holds predicator at 9.0 to 9.3, in its ownmix.exsor in itsmix.lock, moves to 9.4 with it (mix deps.update predicator). With it, a string literal holding a non-ASCII character in an expression reaches_event.dataand aStatifier.Send.BasicHTTPtext body as the UTF-8 string written. Before 2.12.0 each such character became one byte, the low eight bits of its code point, so a text body carrying one was not the literal and was usually not valid UTF-8.- With the same move, a string literal holding a
\uor\Uescape no longer reads as its letters with the backslash dropped. Before 2.12.0'caf\u00e9'read ascafu00e9; write the character itself instead. What a chart sees depends on where the literal sits. In an expression the compiler checks at load (a transition'scond, a<log expr>or a<send><content expr>, for example),Statifier.compile/2refuses the document with an:expression_compile_error. In a<data expr>, an<assign expr>, a<script>or anamelistentry, where the compiler defers the failure, it raiseserror.executionwhen it runs, andStatifier.Publish.findings/2reports it under row S13. Wherever text is read as a value, throughStatifier.Compiler.Expressions.inline_value/1orStatifier.EventData.coerce/1, it no longer folds to a string and is kept as its text, quotes and backslash included: the text of a<data>or an<assign>element; a<content>element's text under<send>,<invoke>or<donedata>; an inbound Basic HTTP request's text body or form parameter values.
A host may start: nothing new.
The conformance corpus changes too. conformance/schema/case.json now
refuses a step's expect_position whose history_values gives a
history state anything but an array of unique, non-empty state ids, and
the library case loan_lost_after_timer states the position after the
step that stops its chart: running false, an empty configuration and
lost still in states_to_invoke. The corpus hash in
conformance/manifest.json moves, so a sibling implementation that
vendors the corpus re-vendors it at the v2.12.0 tag, which is not a
host step.
The conformance runner compares a position's numbers by JSON value: a
float the chart holds agrees with an expected integer of the same value
(2.0 with 2), where before it disagreed and its message named no
member, and a number that differs in value still disagrees, naming the
member. A sibling implementation's runner compares the same way; this is
not a host step either.
2.12.1
A host must change: NONE. The release changes documentation only: no
file under lib/ differs from 2.12.0.
A host may start: nothing new. The conformance corpus does not change,
so a sibling implementation that vendored it at the v2.12.0 tag has
nothing to re-vendor.
2.13.0
A host must change: only where one of these reaches it. A
{:statifier, "~> 2.12"} requirement already accepts 2.13.0.
Statifier.Validator.validate/2, and soStatifier.compile/2, refuses two documents whose<state>has noid: one whoseinitialnames a missing state, reported as{:unresolved_initial, missing_id}, and one with no child states that carries aninitial, reported as{:initial_on_atomic_state, nil}. Before 2.13.0 both validated andStatifier.compile/2raisedKeyError. If your host rescued the raise, match the refusal instead, and if it matchesinitial_on_atomic_state's id as a binary, handleniltoo, as 2.12.0 asked forinitial_not_descendant's parent id.Statifier.Validator.validate/2, and soStatifier.compile/2, decides whether a<state>'sinitialtarget is its descendant by the document's structure for every state, as 2.12.0 began doing for a<history>default target. A document where two states share anidand one'sinitialnames a state under the other was already refused for thatid({:duplicate_id, id}); it now also reports{:initial_not_descendant, target, id}. No document that validated before is refused, and none is accepted that was refused.- A session halted
:budget_exhaustedthat is handed a miss throughStatifier.Session.failed_send/3drops it without stepping, as a session halted:doneor:cancelleddoes. Before 2.13.0 it stepped the halted chart again and sent its subscribers another{:halted, :budget_exhausted}for every miss. A host that waited for that further message after reporting a miss no longer receives it; the miss is the host's dead letter. A session halted:doneor:cancelledis unchanged.
Statifier.Testing.HandlerCase's check that a handler exception
propagates now waits up to ten seconds for the session's exit instead of
one, so a host's generated handler test no longer fails on a loaded
machine; a passing run returns as soon as the exit arrives. This is not a
host step.
A host may start: replacing a running session's registration for one
send type. Requires {:statifier, "~> 2.13"}.
Statifier.Session.replace_send_type/3takes a session, a send type the session already registers and a new registration (a module or{module, opts}, as a:send_typesvalue ofstart_link/2is), and recomputes that type's_ioprocessorsentry in the same call. A host whose front moved a registration's options while the session runs (a new:base_urlforStatifier.Send.BasicHTTP, or a rotated location) hands the session the new registration, the chart reads the new entry from its next event, and later sends of the type go through the new registration. It answers:ok, or changes nothing and answers{:error, :not_running}for a halted session,{:error, :recorded_session}for a session started withrecord: true,{:error, {:unknown_send_type, type}}for a type the session does not register,{:error, {:send_types, {:invalid_registration, type, reason}}}when the processor'scheck_registration/2refuses the registration (asstart_link/2spells that refusal), and{:error, {:ioprocessors_entry, type, exception}}when the entry raises, the session keeping its position. ADR-0069's Amendment of 2026-10-09 records the call.
The conformance corpus's hash does not move: no case changes. The case
schema's descriptions and the corpus README now say that a step's
expected datamodel keeps _name for a document whose <scxml> carries a
name attribute, and that the corpus writes each array of state ids
sorted while the schema does not check the order; no verdict changes.