Invoke Feature Tutorial
This tutorial demonstrates how to use the invoke functionality to manage concurrent, long-running processes. We show two separate examples: a File Downloader and a System Heater.
In SCXML, invoke spawns a child state machine. The parent can:
- Receive automatic
done.invoke.*events when the child reaches a<final>state - Receive custom events sent by the child via
target="#_parent"
Example 1: File Downloader
Parent (main-downloader.scxml)
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" datamodel="ecmascript" initial="idle" name="DownloadManager">
<datamodel>
<data id="downloadProgress" expr="0"/>
</datamodel>
<state id="idle">
<onentry>
<log expr="'=== Download Manager Ready ==='"/>
</onentry>
<transition event="START_DOWNLOAD" target="downloading"/>
</state>
<state id="downloading">
<invoke src="downloader.scxml" id="fileTransfer">
<param name="fileURL" expr="'https://assets.server.com/update.zip'"/>
</invoke>
<!-- Handle progress updates from child -->
<transition event="DOWNLOAD_PROGRESS">
<assign location="downloadProgress" expr="_event.data.percent"/>
<log expr="'Parent: Download progress: ' + downloadProgress + '%'"/>
</transition>
<!-- Handle completion (child reached <final>) -->
<transition event="done.invoke.fileTransfer" target="complete"/>
</state>
<state id="complete">
<onentry>
<log expr="'=== Download Complete! ==='"/>
</onentry>
</state>
</scxml>
Child (downloader.scxml)
<scxml version="1.0" xmlns="http://www.w3.org/2005/07/scxml" datamodel="ecmascript" initial="connecting">
<datamodel>
<data id="fileURL"/>
<data id="progress" expr="0"/>
</datamodel>
<state id="connecting">
<onentry>
<log expr="'Connecting to: ' + fileURL"/>
<send event="CONNECTED" delay="300ms"/>
</onentry>
<transition event="CONNECTED" target="transferring"/>
</state>
<state id="transferring">
<onentry>
<send event="PROGRESS_TICK" delay="200ms"/>
</onentry>
<transition event="PROGRESS_TICK" cond="progress < 100">
<assign location="progress" expr="progress + 25"/>
<!-- Send progress to parent -->
<send event="DOWNLOAD_PROGRESS" target="#_parent">
<param name="percent" expr="progress"/>
</send>
<send event="PROGRESS_TICK" delay="200ms"/>
</transition>
<transition cond="progress >= 100" target="success"/>
</state>
<final id="success"/>
</scxml>
Testing the Downloader
- Open
main-downloader.scxmlin the simulator - Send
START_DOWNLOADevent - Watch the logs - you'll see:
- Child connecting and transferring
- Parent receiving
DOWNLOAD_PROGRESSevents with percentages - Automatic
done.invoke.fileTransferwhen child reaches<final>
Example 2: System Heater
Parent (main-heater.scxml)
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" datamodel="ecmascript" initial="off" name="HeaterController">
<datamodel>
<data id="currentTemp" expr="0"/>
<data id="targetTemperature" expr="70"/>
</datamodel>
<state id="off">
<onentry>
<log expr="'=== Heater Controller Ready ==='"/>
</onentry>
<transition event="HEAT_ON" target="heating"/>
</state>
<state id="heating">
<invoke src="heater.scxml" id="heaterControl">
<param name="targetTemp" expr="targetTemperature"/>
</invoke>
<!-- Handle temperature updates from child -->
<transition event="HEATER_TEMP_UPDATE">
<assign location="currentTemp" expr="_event.data.temp"/>
<log expr="'Parent: Temperature now ' + currentTemp + ' degrees'"/>
</transition>
<!-- Handle completion (child reached target) -->
<transition event="done.invoke.heaterControl" target="target_reached"/>
<!-- Allow manual shutoff -->
<transition event="HEAT_OFF" target="off"/>
</state>
<state id="target_reached">
<onentry>
<log expr="'=== Target Temperature Reached: ' + currentTemp + ' degrees ==='"/>
</onentry>
</state>
</scxml>
Child (heater.scxml)
<scxml version="1.0" xmlns="http://www.w3.org/2005/07/scxml" datamodel="ecmascript" initial="warming_up">
<datamodel>
<data id="temperature" expr="20"/>
<data id="targetTemp" expr="70"/>
</datamodel>
<state id="warming_up">
<onentry>
<log expr="'Heater: Starting at ' + temperature + ' degrees'"/>
<send event="HEAT_TICK" delay="500ms"/>
</onentry>
<transition event="HEAT_TICK">
<assign location="temperature" expr="temperature + 10"/>
<!-- Send temperature to parent -->
<send event="HEATER_TEMP_UPDATE" target="#_parent">
<param name="temp" expr="temperature"/>
</send>
<send event="HEAT_TICK" delay="500ms"/>
</transition>
<transition cond="temperature >= targetTemp" target="at_temperature"/>
</state>
<final id="at_temperature"/>
</scxml>
Testing the Heater
- Open
main-heater.scxmlin the simulator - Send
HEAT_ONevent - Watch the logs - you'll see:
- Child warming up incrementally
- Parent receiving
HEATER_TEMP_UPDATEevents with temperature values - Automatic
done.invoke.heaterControlwhen target is reached
Key Concepts
1. Child-to-Parent Communication
The child doesn't know the parent's name. Use the reserved target #_parent:
<send event="MY_UPDATE" target="#_parent">
<param name="value" expr="someValue"/>
</send>
The parent receives this in _event.data:
<transition event="MY_UPDATE">
<assign location="myVar" expr="_event.data.value"/>
</transition>
2. Automatic Done Events
When a child reaches a <final> state, the parent automatically receives done.invoke.{id}:
<invoke src="child.scxml" id="myChild"/>
<transition event="done.invoke.myChild" target="finished"/>
3. Passing Data to Child (<param>)
<invoke src="child.scxml" id="myChild">
<param name="url" expr="myURL"/>
<param name="timeout" expr="5000"/>
</invoke>
The child declares matching <data> elements to receive these values.
4. Cancellation
If the parent exits the state containing the <invoke>, the child is automatically cancelled.
<state id="working">
<invoke src="longTask.scxml" id="task"/>
<transition event="CANCEL" target="idle"/> <!-- Exiting cancels the child -->
</state>
Observing Child Behavior
The simulator provides powerful tools to observe and debug parent-child state machine interactions.
Invoke Context in the TUI
By default, the REPL displays only parent machine events. Child machine events are hidden to reduce noise. All trace events include invoke context information, prefixed with [invokeId] when from a child.
Filtering with the watch Command
Use the watch command to control which invoke contexts you see:
watch <type> <filter>
Types: states, events, transitions, vars, scheduled, log, actions
Filters:
- (empty) - Parent machine only (default)
parent- Explicitly show parent<invokeId>- Show a specific child (e.g.,heaterControl)*- Show all (parent + all children)off- Disable this trace type entirely
Examples:
watch log * # See all logs from parent and children
watch states heaterControl # See state changes only from heaterControl child
watch events * # See all events from everywhere
watch vars off # Stop showing variable changes
Event Data Visualization
When events carry data payloads (e.g., from <send> with <param>), the simulator displays them:
⚡ Event: HEATER_TEMP_UPDATE {temp: 30}
⚡ Event: DOWNLOAD_PROGRESS {percent: 50}
This makes it easy to see what data the child is sending to the parent via #_parent.
Log Output
<log> actions from state machines are displayed in the REPL:
<log expr="'Temperature: ' + temperature"/>
Shows as:
Temperature: 30
Child logs are prefixed with their invoke ID:
[heaterControl] Heater: Starting at 20 degrees
Trace Recording with Invoke Context
Trace recordings capture the full invoke context, allowing you to see which machine (parent or child) generated each event.
Start recording:
trace record my-session.jsonl
Stop recording:
trace stop
The trace file includes invoke_id for child machine events:
{"timestamp":12345,"type":"state_enter","state_id":"warming_up","invoke_id":"heaterControl"}
{"timestamp":12350,"type":"event_received","event_name":"HEATER_TEMP_UPDATE","event_data":{"temp":30}}
Embed Traces in SCXML Files
You can embed recorded traces directly in your SCXML files for test automation:
trace embed my-test-case
This stores the trace in the SCXML file under a custom namespace extension.
Event Data in Parent Variables
When a child sends an event with data to the parent:
Child (heater.scxml):
<send event="HEATER_TEMP_UPDATE" target="#_parent">
<param name="temp" expr="temperature"/>
<param name="status" expr="'heating'"/>
</send>
Parent receives:
_event.name="HEATER_TEMP_UPDATE"_event.data.temp= the temperature value_event.data.status="heating"
Parent can assign to its own variables:
<transition event="HEATER_TEMP_UPDATE">
<assign location="currentTemp" expr="_event.data.temp"/>
<assign location="heaterStatus" expr="_event.data.status"/>
</transition>
Advanced: Parent-to-Child Communication
While SCXML doesn't support the parent directly modifying child variables, the parent can send events to the child:
<send event="SET_TARGET" target="#_heaterControl">
<param name="newTarget" expr="75"/>
</send>
The child receives this event and can update its own variables:
<transition event="SET_TARGET">
<assign location="targetTemp" expr="_event.data.newTarget"/>
</transition>
Note: The target uses #_ prefix followed by the invoke ID.
Debugging Tips
- Use
watch log *to see all log output during development - Use
watch events *to trace the full event flow between parent and child - Record traces with
trace recordto capture complex interactions for later analysis - Check event data - the TUI shows event payloads like
{temp: 30} - Filter by invoke ID - focus on specific children when debugging complex systems