11. Composing machines: submachine invoke¶
Sometimes a step is itself a whole machine you’d rather keep separate — payment authorization,
a KYC check, a credit decision. invoke runs another machine as a child, treats it as a
black box, and routes on the verdict it returns.
Run it¶
import sys, tempfile
from pathlib import Path
from harel import definition_from_dsl_file, DurableRunner, DictStore
project = Path(tempfile.mkdtemp())
sys.path.insert(0, str(project))
# the submachine's decision logic, referenced as a literal dotted path
(project / "paylib.py").write_text(
"def decide(stm, event, **inputs):\n"
" return 'ok' if stm.execution_ctx.get('amount', 0) <= 100 else 'no'\n"
)
(project / "payment.stm").write_text("""
machine payment {
carry authcode
initial Authorizing
state Authorizing {}
final Authorized success {}
final Declined failed {}
from Authorizing select paylib.decide {
"ok" to Authorized
"no" to Declined
}
}
""")
(project / "order.stm").write_text("""
import "payment.stm" as pay
machine order {
initial Charge
state Charge {
invoke pay.payment
with { amount: amount }
}
final Paid success
final Cancelled cancelled
from Charge to Paid on Returned where outcome == "success"
from Charge to Cancelled on Returned where outcome == "failed"
}
""")
defn = definition_from_dsl_file(project / "order.stm", "order")
runner = DurableRunner(DictStore(), {defn.id: defn})
for amount in (50, 500):
exe = runner.create(defn.id, context={"amount": amount})
print(f"amount={amount:<4} -> {exe.active_path} / {exe.outcome}")
amount=50 -> Paid / success
amount=500 -> Cancelled / cancelled
Entering Charge forks the payment machine as a child execution seeded with amount. The
child runs to its own terminal, and on finishing emits a Returned event carrying its
outcome (and anything it declared with carry). The order routes on that event: a small
amount is Authorized → Paid; a large one is Declined → Cancelled.
Black box, in and out¶
Important
A submachine is a black box. The parent’s domain events are not delivered into it — so
a submachine must drive itself to completion (automatic transitions and selectors), which is
why payment uses an automatic select. Communication is narrow and explicit:
in —
with { child_key: parent_key }projects parent context into the child;out — the child’s terminal
outcome, plus any context keys it lists withcarry, arrive on theReturnedevent.
A single invoke state must be a leaf with no automatic exit — it leaves only by routing
on Returned. (Lifecycle Cancel still reaches a running submachine; that’s the control
plane, in its own guide.)
Invoking one submachine is composition. Invoking the same machine once per item in a list — ship N parcels, each its own run — is the data-parallel sibling: fan-out.