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:
- Phase 1 (Collection): Collects name and email (simulated with delays)
- Phase 2 (Validation): Validates format and content
- Phase 3 (Submission): Prepares and submits data
- Each phase enters a
<final>state, triggeringdone.state.phaseX - 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 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)
<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:
<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.*:
<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
<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
<transition event="done.state.validation" target="approved"
cond="_event.data.score >= 80"/>
<transition event="done.state.validation" target="review"
cond="_event.data.score >= 50 && _event.data.score < 80"/>
<transition event="done.state.validation" target="rejected"
cond="_event.data.score < 50"/>
Parallel Phase Completion
With parallel states, you can wait for multiple phases:
<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:
<final id="analysis_done">
<donedata>
<content expr="{
summary: generateSummary(),
details: analysisResults,
metrics: {
accuracy: calculateAccuracy(),
confidence: confidenceScore
},
recommendations: getRecommendations()
}"/>
</donedata>
</final>
Code Generation
Java
scxml-gen done-events.scxml -t java -o DoneEventsDemo.java --package com.example
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
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
#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
<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
<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
<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:
<!-- ❌ 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
<!-- ❌ 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>:
<!-- ❌ 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
- Invoke Feature -
done.invoke.*events from child machines - Error Handling - Combining done events with error recovery
- Deep History - Preserving state across phase changes
- Feature Matrix - Complete SCXML element support