Done Events & Donedata

Done Events & Donedata

Master compound state completion with done.state.* events and data passing via <donedata>. This example demonstrates how to build multi-phase workflows where each phase reports completion with structured results.

What you'll learn:

  • done.state.* events: automatic events when compound states complete
  • <donedata> element: passing structured data with completion events
  • <final> states: marking completion points in compound states
  • Multi-phase workflow orchestration
  • Aggregating results across phases

Try It

Click Start and watch the automated workflow progress through three phases. Each phase completes automatically and passes data to the parent:

What's happening:

  1. Phase 1 (Collection): Collects name and email (simulated with delays)
  2. Phase 2 (Validation): Validates format and content
  3. Phase 3 (Submission): Prepares and submits data
  4. Each phase enters a <final> state, triggering done.state.phaseX
  5. Parent workflow captures the <donedata> and aggregates results

The Core Concept

When a compound state contains a <final> child and that final state becomes active, SCXML automatically generates a done.state.{id} event that the parent state can handle.

┌─────────────────────── workflow ────────────────────────┐
│                                                         │
│   done.state.phase1        done.state.phase2           │
│         │                        │                      │
│         ▼                        ▼                      │
│   ┌─────────────┐          ┌─────────────┐             │
│   │   phase1    │──next───▶│   phase2    │──next──▶ ...│
│   │  ┌───────┐  │          │  ┌───────┐  │             │
│   │  │ final │  │          │  │ final │  │             │
│   │  │┌─────┐│  │          │  │┌─────┐│  │             │
│   │  ││done-││  │          │  ││done-││  │             │
│   │  ││data ││  │          │  ││data ││  │             │
│   │  │└─────┘│  │          │  │└─────┘│  │             │
│   │  └───────┘  │          │  └───────┘  │             │
│   └─────────────┘          └─────────────┘             │
│                                                         │
└─────────────────────────────────────────────────────────┘

The SCXML Implementation

xml
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
       datamodel="ecmascript" initial="workflow" name="DoneEventsDemo">

  <datamodel>
    <data id="totalSteps" expr="0"/>
    <data id="results" expr="[]"/>
  </datamodel>

  <state id="workflow" initial="phase1">
    <onentry>
      <log label="Workflow" expr="'Starting multi-phase workflow'"/>
      <assign location="results" expr="[]"/>
    </onentry>

    <!-- Handle phase completions with their donedata -->
    <transition event="done.state.phase1" target="phase2">
      <script>
        results.push({
          phase: _event.data.phase,
          items: _event.data.itemsCollected
        });
        totalSteps = totalSteps + _event.data.itemsCollected;
      </script>
      <log label="Workflow" expr="'Phase 1 done! Data: ' + JSON.stringify(_event.data)"/>
    </transition>

    <transition event="done.state.phase2" target="phase3">
      <script>
        results.push({
          phase: _event.data.phase,
          valid: _event.data.valid
        });
        totalSteps = totalSteps + _event.data.checksPerformed;
      </script>
      <log label="Workflow" expr="'Phase 2 done! Data: ' + JSON.stringify(_event.data)"/>
    </transition>

    <transition event="done.state.phase3" target="complete">
      <script>
        results.push({
          phase: _event.data.phase,
          success: _event.data.success
        });
        totalSteps = totalSteps + 2;
      </script>
      <log label="Workflow" expr="'Phase 3 done! Data: ' + JSON.stringify(_event.data)"/>
    </transition>

    <transition event="cancel" target="cancelled"/>

    <!-- Phase 1: Data Collection -->
    <state id="phase1" initial="collect_name">
      <onentry>
        <log label="Phase1" expr="'Phase 1: Data Collection'"/>
      </onentry>

      <state id="collect_name">
        <onentry>
          <log label="Step" expr="'Collecting name...'"/>
          <send event="name_collected" delay="500ms"/>
        </onentry>
        <transition event="name_collected" target="collect_email"/>
      </state>

      <state id="collect_email">
        <onentry>
          <log label="Step" expr="'Collecting email...'"/>
          <send event="email_collected" delay="500ms"/>
        </onentry>
        <transition event="email_collected" target="phase1_done"/>
      </state>

      <!-- Final state with donedata -->
      <final id="phase1_done">
        <onentry>
          <log label="Phase1" expr="'Phase 1 complete!'"/>
        </onentry>
        <donedata>
          <param name="phase" expr="'collection'"/>
          <param name="itemsCollected" expr="2"/>
        </donedata>
      </final>
    </state>

    <!-- Phase 2: Validation -->
    <state id="phase2" initial="validate_format">
      <onentry>
        <log label="Phase2" expr="'Phase 2: Validation'"/>
      </onentry>

      <state id="validate_format">
        <onentry>
          <log label="Step" expr="'Validating format...'"/>
          <send event="format_ok" delay="300ms"/>
        </onentry>
        <transition event="format_ok" target="validate_content"/>
      </state>

      <state id="validate_content">
        <onentry>
          <log label="Step" expr="'Validating content...'"/>
          <send event="content_ok" delay="300ms"/>
        </onentry>
        <transition event="content_ok" target="phase2_done"/>
      </state>

      <final id="phase2_done">
        <donedata>
          <param name="phase" expr="'validation'"/>
          <param name="checksPerformed" expr="2"/>
          <param name="valid" expr="true"/>
        </donedata>
      </final>
    </state>

    <!-- Phase 3: Submission -->
    <state id="phase3" initial="prepare">
      <onentry>
        <log label="Phase3" expr="'Phase 3: Submission'"/>
      </onentry>

      <state id="prepare">
        <onentry>
          <log label="Step" expr="'Preparing submission...'"/>
          <send event="prepared" delay="200ms"/>
        </onentry>
        <transition event="prepared" target="submit"/>
      </state>

      <state id="submit">
        <onentry>
          <log label="Step" expr="'Submitting...'"/>
          <send event="submitted" delay="500ms"/>
        </onentry>
        <transition event="submitted" target="phase3_done"/>
      </state>

      <final id="phase3_done">
        <donedata>
          <param name="phase" expr="'submission'"/>
          <param name="success" expr="true"/>
          <param name="timestamp" expr="Date.now()"/>
        </donedata>
      </final>
    </state>
  </state>

  <state id="complete">
    <onentry>
      <log label="Workflow" expr="'=== WORKFLOW COMPLETE ==='"/>
      <log label="Summary" expr="'Total steps: ' + totalSteps"/>
      <log label="Results" expr="JSON.stringify(results, null, 2)"/>
    </onentry>
    <transition event="restart" target="workflow"/>
  </state>

  <state id="cancelled">
    <onentry>
      <log label="Workflow" expr="'Workflow cancelled'"/>
    </onentry>
    <transition event="restart" target="workflow"/>
  </state>
</scxml>

Key Concepts

Final States Generate Done Events

When a <final> state becomes active inside a compound state, SCXML generates:

  • Event name: done.state.{parent-id}
  • Event data: Contents of <donedata> (if present)
xml
<state id="phase1">
  <!-- ... child states ... -->

  <final id="phase1_done">
    <!-- This triggers: done.state.phase1 -->
  </final>
</state>

Donedata Structure

Pass structured data with the completion event:

xml
<final id="task_complete">
  <donedata>
    <!-- Static values -->
    <param name="status" expr="'success'"/>

    <!-- Dynamic expressions -->
    <param name="itemCount" expr="items.length"/>
    <param name="timestamp" expr="Date.now()"/>

    <!-- Complex objects -->
    <param name="summary" expr="{ processed: count, errors: errorList }"/>
  </donedata>
</final>

Accessing Donedata

In the transition handling done.state.*:

xml
<transition event="done.state.phase1" target="phase2">
  <!-- Access via _event.data -->
  <log label="Phase" expr="'Completed: ' + _event.data.phase"/>
  <log label="Items" expr="'Collected: ' + _event.data.itemsCollected"/>

  <!-- Store for later use -->
  <assign location="phaseResults" expr="_event.data"/>
</transition>

Advanced Patterns

Aggregating Results Across Phases

xml
<datamodel>
  <data id="workflowResults" expr="{
    startTime: null,
    endTime: null,
    phases: [],
    totalDuration: 0
  }"/>
</datamodel>

<state id="workflow" initial="phase1">
  <onentry>
    <assign location="workflowResults.startTime" expr="Date.now()"/>
    <assign location="workflowResults.phases" expr="[]"/>
  </onentry>

  <transition event="done.state.phase1" target="phase2">
    <script>
      workflowResults.phases.push({
        name: 'phase1',
        result: _event.data,
        completedAt: Date.now()
      });
    </script>
  </transition>

  <!-- ... more phases ... -->

  <transition event="done.state.phase3" target="complete">
    <script>
      workflowResults.endTime = Date.now();
      workflowResults.totalDuration =
        workflowResults.endTime - workflowResults.startTime;
    </script>
  </transition>
</state>

Conditional Phase Routing

xml
<transition event="done.state.validation" target="approved"
            cond="_event.data.score >= 80"/>

<transition event="done.state.validation" target="review"
            cond="_event.data.score >= 50 &amp;&amp; _event.data.score &lt; 80"/>

<transition event="done.state.validation" target="rejected"
            cond="_event.data.score &lt; 50"/>

Parallel Phase Completion

With parallel states, you can wait for multiple phases:

xml
<parallel id="parallel_phases">
  <state id="download" initial="...">
    <final id="download_done">
      <donedata>
        <param name="bytes" expr="downloadedBytes"/>
      </donedata>
    </final>
  </state>

  <state id="process" initial="...">
    <final id="process_done">
      <donedata>
        <param name="items" expr="processedItems"/>
      </donedata>
    </final>
  </state>

  <!-- This fires when BOTH children reach final states -->
  <transition event="done.state.parallel_phases" target="complete"/>
</parallel>

Content-Based Donedata

Use <content> for complex data structures:

xml
<final id="analysis_done">
  <donedata>
    <content expr="{
      summary: generateSummary(),
      details: analysisResults,
      metrics: {
        accuracy: calculateAccuracy(),
        confidence: confidenceScore
      },
      recommendations: getRecommendations()
    }"/>
  </donedata>
</final>

Code Generation

Java

bash
scxml-gen done-events.scxml -t java -o DoneEventsDemo.java --package com.example
java
DoneEventsDemo sm = new DoneEventsDemo();

// Listen for state changes
sm.addStateListener((oldStates, newStates) -> {
    if (newStates.contains("complete")) {
        // Access aggregated results
        Object results = sm.getDataModel().get("results");
        System.out.println("Workflow complete: " + results);
    }
});

try (var executor = new ContinuousStateMachineExecutor(sm)) {
    executor.start();

    // Workflow runs automatically via delayed events
    Thread.sleep(5000);

    // Check results
    System.out.println("Total steps: " + sm.getDataModel().get("totalSteps"));
}

JavaScript

javascript
import { DoneEventsDemo } from './DoneEventsDemo.js';

const workflow = new DoneEventsDemo();

workflow.onStateChange(() => {
    const states = [...workflow.getActiveStateIds()];
    console.log('Active states:', states);

    if (states.includes('complete')) {
        // Workflow finished - access results
        console.log('Results:', workflow.datamodel.results);
        console.log('Total steps:', workflow.datamodel.totalSteps);
    }
});

workflow.start();
// Phases execute automatically

C

c
#include "done_events_demo.h"

DoneEventsDemo sm;
DoneEventsDemo_init(&sm);
DoneEventsDemo_start(&sm);

// Process delayed events in your main loop
while (!DoneEventsDemo_is_in(&sm, S_COMPLETE) &&
       !DoneEventsDemo_is_in(&sm, S_CANCELLED)) {

    DoneEventsDemo_tick(&sm, get_elapsed_ms());
    sleep_ms(10);
}

// Check results
printf("Total steps: %d\n", sm.data.totalSteps);

Use Cases

Form Wizard

xml
<state id="registration_wizard" initial="personal_info">
  <state id="personal_info">
    <!-- Form fields, validation -->
    <final id="personal_done">
      <donedata>
        <param name="name" expr="formData.name"/>
        <param name="email" expr="formData.email"/>
      </donedata>
    </final>
  </state>

  <transition event="done.state.personal_info" target="preferences">
    <assign location="userData.personal" expr="_event.data"/>
  </transition>

  <state id="preferences">...</state>
  <state id="confirmation">...</state>
</state>

Order Processing Pipeline

xml
<state id="order_pipeline" initial="validate_order">
  <transition event="done.state.validate_order" target="check_inventory"
              cond="_event.data.valid"/>
  <transition event="done.state.validate_order" target="order_invalid"
              cond="!_event.data.valid"/>

  <transition event="done.state.check_inventory" target="process_payment"
              cond="_event.data.inStock"/>
  <transition event="done.state.check_inventory" target="out_of_stock"
              cond="!_event.data.inStock"/>

  <transition event="done.state.process_payment" target="ship_order"/>
  <transition event="done.state.ship_order" target="complete"/>
</state>

Test Suite Runner

xml
<state id="test_suite" initial="setup">
  <transition event="done.state.setup" target="run_tests"/>
  <transition event="done.state.run_tests" target="teardown">
    <assign location="testResults" expr="_event.data.results"/>
  </transition>
  <transition event="done.state.teardown" target="report">
    <script>generateReport(testResults);</script>
  </transition>
</state>

Common Mistakes

1. Forgetting That Done Events Are Internal

Done events are raised internally and can only be caught by the parent state:

xml
<!-- ❌ Won't work: sibling can't see done.state.phase1 -->
<state id="phase1">
  <final id="phase1_done"/>
</state>
<state id="phase2">
  <transition event="done.state.phase1" target="..."/>  <!-- Never fires! -->
</state>

<!-- ✅ Correct: parent handles done events -->
<state id="workflow">
  <transition event="done.state.phase1" target="phase2"/>  <!-- Works! -->
  <state id="phase1"><final id="phase1_done"/></state>
  <state id="phase2">...</state>
</state>

2. Missing Donedata Access

xml
<!-- ❌ Wrong: _event.data might not exist -->
<assign location="x" expr="_event.data.value"/>

<!-- ✅ Safe: check first or use default -->
<assign location="x" expr="_event.data ? _event.data.value : defaultValue"/>

3. Expecting Done Events from Atomic States

Done events only fire from compound states with <final>:

xml
<!-- ❌ No done event: atomic state with final id doesn't work -->
<state id="simple">
  <final id="simple_done"/>  <!-- This IS the state, not a child -->
</state>

<!-- ✅ Correct: compound state with final child -->
<state id="compound">
  <state id="working">...</state>
  <final id="compound_done"/>  <!-- Child of compound -->
</state>

Summary

Concept Description Syntax
<final> Marks completion of compound state <final id="done"/>
Done event Auto-generated on final state entry done.state.{parent-id}
<donedata> Data payload for done event <donedata><param .../></donedata>
Event access Get donedata in transition _event.data.paramName

Files

File Description
done-events.scxml SCXML source file
done-events-player.html Interactive demo

Next Steps