Invoke Feature Tutorial

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:

  1. Receive automatic done.invoke.* events when the child reaches a <final> state
  2. Receive custom events sent by the child via target="#_parent"

Example 1: File Downloader

Parent (main-downloader.scxml)

xml
<?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)

xml
<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 &lt; 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

  1. Open main-downloader.scxml in the simulator
  2. Send START_DOWNLOAD event
  3. Watch the logs - you'll see:
    • Child connecting and transferring
    • Parent receiving DOWNLOAD_PROGRESS events with percentages
    • Automatic done.invoke.fileTransfer when child reaches <final>

Example 2: System Heater

Parent (main-heater.scxml)

xml
<?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)

xml
<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

  1. Open main-heater.scxml in the simulator
  2. Send HEAT_ON event
  3. Watch the logs - you'll see:
    • Child warming up incrementally
    • Parent receiving HEATER_TEMP_UPDATE events with temperature values
    • Automatic done.invoke.heaterControl when target is reached

Key Concepts

1. Child-to-Parent Communication

The child doesn't know the parent's name. Use the reserved target #_parent:

xml
<send event="MY_UPDATE" target="#_parent">
    <param name="value" expr="someValue"/>
</send>

The parent receives this in _event.data:

xml
<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}:

xml
<invoke src="child.scxml" id="myChild"/>
<transition event="done.invoke.myChild" target="finished"/>

3. Passing Data to Child (<param>)

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

xml
<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:

xml
<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:

json
{"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):

xml
<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:

xml
<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:

xml
<send event="SET_TARGET" target="#_heaterControl">
    <param name="newTarget" expr="75"/>
</send>

The child receives this event and can update its own variables:

xml
<transition event="SET_TARGET">
    <assign location="targetTemp" expr="_event.data.newTarget"/>
</transition>

Note: The target uses #_ prefix followed by the invoke ID.


Debugging Tips

  1. Use watch log * to see all log output during development
  2. Use watch events * to trace the full event flow between parent and child
  3. Record traces with trace record to capture complex interactions for later analysis
  4. Check event data - the TUI shows event payloads like {temp: 30}
  5. Filter by invoke ID - focus on specific children when debugging complex systems