Go Target Documentation
VSCXML generates Go 1.21+ code from W3C SCXML state machine specifications.
W3C Compliance Status
| Datamodel | Status | Tests Passing |
|---|---|---|
| ECMAScript (Goja) | 100% | 183/183 |
| Null | 100% | 6/6 |
| Native-Go | 100% | 20/20 |
The Go target achieves full W3C SCXML compliance across all three datamodels, matching Java, JavaScript, C#, C, and Python targets.
Last Verified: 2026-02-01
Requirements
- Go 1.21 or later
github.com/dop251/goja(for ECMAScript datamodel)github.com/google/uuid(for session IDs)
Installation
Bundled Project (Default — Recommended)
The generator produces a ready-to-build Go module with the runtime bundled locally:
scxml-gen input.scxml -t go -o output/MyMachine.go
cd output/
go mod tidy # downloads goja and uuid
go build ./...
go run .
Generated project layout:
output/
├── MyMachine.go # Generated state machine (package main)
├── main.go # Ready-to-run demo entry point
├── go.mod # Module definition (e.g. example.com/mymachine)
├── README.md
└── scxmlgen/ # Bundled runtime package
├── transpiled_state_machine.go
├── executor.go
├── trace.go
└── ...
Code-only mode: Use
--code-onlyto generate just the.gofile when integrating into an existing module that already has thescxmlgenpackage.
Published Package
For projects that prefer a versioned dependency:
go get github.com/scxmlgen/scxmlgen
Code Generation
Generate Go code using the CLI:
# Full project (default) — generates go.mod, main.go, scxmlgen/ runtime, README
scxml-gen input.scxml -t go -o output/MyMachine.go
# Code only — single .go file, no project scaffold (for existing modules)
scxml-gen input.scxml -t go -o MyMachine.go --code-only
# Custom module name (used in go.mod and runtime import path)
scxml-gen input.scxml -t go -o output/MyMachine.go --package github.com/myorg/myproject
# Bundle invoke children
scxml-gen parent.scxml -t go -o output/Parent.go --bundle-auto
Usage
Basic Example
package main
import (
"fmt"
// Bundled runtime — use your module path (set by generator in go.mod):
"example.com/mymachine/scxmlgen"
// Or the published package when using `go get github.com/scxmlgen/scxmlgen`:
// "github.com/scxmlgen/scxmlgen"
)
func main() {
// Create and start
machine := NewMyStateMachine()
machine.Start()
// Send events
machine.Send(scxmlgen.NewEvent("go"))
machine.Send(scxmlgen.NewEventBuilder().
Name("complete").
Data(map[string]interface{}{"result": 42}).
Build())
// Check current state
if machine.IsInState("active") {
fmt.Println("Machine is active")
}
// Check completion
if machine.IsFinished() {
fmt.Println("Machine has finished")
}
}
Event Handling
import "example.com/mymachine/scxmlgen"
machine := NewMyStateMachine()
machine.Start()
// String-based (convenient) — Send() accepts *scxmlgen.Event
machine.Send(scxmlgen.NewEvent("start"))
// Event with data
data := map[string]interface{}{"count": 5, "name": "test"}
machine.Send(scxmlgen.NewEventBuilder().Name("update").Data(data).Build())
// Full event construction (via runtime context) — advanced use
event := scxmlgen.NewEventBuilder().
Name("custom").
Origin("#_session_123").
OriginType("http://www.w3.org/TR/scxml/#SCXMLEventProcessor").
Build()
machine.GetRuntimeContext().EnqueueExternal(event)
machine.Pump()
Executor (Thread-Safe, Autonomous Timers)
ContinuousExecutor runs the machine on a dedicated goroutine. When the machine implements
IRuntimeAwareStateMachine (all generated machines do), the executor registers a wakeup
listener so that timer-fired events (<send delay="..."/>) are processed automatically —
no manual Pump() calls required.
import "example.com/mymachine/scxmlgen"
machine := NewMyStateMachine()
machine.Start()
executor := scxmlgen.NewContinuousExecutor(machine)
// Register callbacks (optional)
executor.OnFinished(func() {
fmt.Println("State machine finished")
})
// Start the processing goroutine (also registers autonomous wakeup listener)
executor.Start()
// Send events (thread-safe, non-blocking)
executor.SendEventAsync(scxmlgen.NewEvent("event1"))
executor.SendAsync("event2")
// Wait for the machine to reach a <final> state
executor.Wait()
// Or stop manually
executor.Stop()
Data Models
ECMAScript (Default)
Uses Goja for JavaScript expression evaluation. Goja is a pure Go implementation of ECMAScript 5.1+.
<scxml datamodel="ecmascript">
<datamodel>
<data id="counter" expr="0"/>
<data id="items" expr="[]"/>
</datamodel>
<state id="counting">
<onentry>
<script>counter = counter + 1; items.push(counter);</script>
</onentry>
<transition cond="counter >= 10" target="done"/>
</state>
</scxml>
Go usage:
machine := NewMyMachine()
machine.Start()
fmt.Println(machine.DataModel.Get("counter")) // 10
fmt.Println(machine.DataModel.Get("items")) // [1 2 3 ... 10]
Null Datamodel
Minimal datamodel with only In() predicate support. No variables.
<scxml datamodel="null">
<state id="s0">
<transition cond="In('s1')" target="fail"/>
<transition target="pass"/>
</state>
</scxml>
Native Go
Type-safe Go variables with compile-time code generation. Expressions are transformed to Go syntax at generation time.
<scxml datamodel="native-go">
<datamodel>
<data id="counter" type="int" expr="0"/>
<data id="name" type="string" expr=""default""/>
<data id="items" type="[]int" expr="nil"/>
</datamodel>
<state id="s0">
<transition cond="counter > 5" target="done"/>
<onentry>
<assign location="counter" expr="counter + 1"/>
</onentry>
</state>
</scxml>
Generated code:
type MyMachine struct {
scxmlgen.TranspiledStateMachine
counter int
name string
items []int
}
func NewMyMachine() *MyMachine {
sm := &MyMachine{}
sm.Init("MyMachine", scxmlgen.DataModelNativeGo, 2)
sm.counter = 0
sm.name = "default"
sm.items = nil
return sm
}
func (sm *MyMachine) enterS_S0() {
sm.ActiveStates[S_S0] = struct{}{}
sm.counter = sm.counter + 1 // Direct Go
}
func (sm *MyMachine) stateS_S0(eventID int, event *scxmlgen.Event) bool {
if sm.counter > 5 { // Direct Go condition
sm.exitS_S0()
sm.enterS_DONE()
return true
}
return false
}
Supported Types
int,int64,float64,bool,string[]T(slices),map[K]V(maps)interface{}for untyped variables
Generated Code Structure
The generator produces a single Go file with:
State Constants: Integer indices for O(1) lookups
goconst ( S_IDLE = 0 S_RUNNING = 1 )Event Constants: Integer IDs for fast dispatch
goconst ( EVT_UNKNOWN = 0 EVT_START = 1 EVT_STOP = 2 )State Methods: Enter/exit/handler for each state
gofunc (sm *MyMachine) enterS_IDLE() func (sm *MyMachine) exitS_IDLE() func (sm *MyMachine) stateS_IDLE(eventID int, event *scxmlgen.Event) boolEventless Transitions: Three-phase execution for W3C compliance
gofunc (sm *MyMachine) checkEventlessTransitions() bool
Advanced Features
Delayed Events
Delayed sends are scheduled via Go's time.AfterFunc in the runtime context:
<send event="timeout" delay="5s"/>
With ContinuousExecutor (recommended): timer-fired events are processed automatically
via the wakeup listener — no polling loop needed:
machine.Start()
executor := scxmlgen.NewContinuousExecutor(machine)
executor.Start()
executor.Wait() // blocks until machine finishes or executor.Stop() is called
Manual usage (without executor): call Pump() periodically to drain timer-fired events:
machine.Start()
for !machine.IsFinished() {
machine.Pump()
time.Sleep(50 * time.Millisecond)
}
Invoke (Child State Machines)
Child state machines are bundled at generation time:
scxml-gen parent.scxml -t go -o Parent.go --bundle-auto
The generated code includes an _invokeRegistry for child lookups:
var _invokeRegistry = map[string]func() interface{}{
"file:child.scxml": func() interface{} { return NewParent_External_0() },
"s0.invoke.0": func() interface{} { return NewParent_Child_0() },
}
History States
Both shallow and deep history are supported:
// Shallow: remembers direct child
history_H0 int
// Deep: remembers all active descendants
history_DEEP_H map[int]struct{}
Parallel States
Parallel regions are entered/exited correctly:
func (sm *MyMachine) enterS_PARALLEL() {
sm.ActiveStates[S_PARALLEL] = struct{}{}
// Enter all parallel children
sm.enterS_REGION1()
sm.enterS_REGION2()
}
Testing
Run W3C conformance tests (when available):
cd scxml-go/test
# All tests
go test -v -run TestW3C
# Specific test
go test -v -run TestW3C/test144
# Specific datamodel
go test -v -run TestW3C -datamodel ecmascript
Performance Considerations
- State tracking: Uses Go
map[int]struct{}for O(1) state checks - Event dispatch: Switch statement with integer constants
- Memory: Generated code has minimal runtime overhead
- Goja: JavaScript evaluation adds ~5-10MB memory, ~1ms per expression
- Native-Go: Zero overhead for expressions (compiled directly into Go)
Troubleshooting
Import Error
If the scxmlgen package is not found:
# If using go modules
go mod init myproject
go get github.com/scxmlgen/scxmlgen
# Or copy bundled runtime
scxml-gen input.scxml -t go -o output/
# This creates scxmlgen/ directory
Goja Not Found
go get github.com/dop251/goja
Go Version Error
The generated code requires Go 1.21+. Check your version:
go version
API Reference
TranspiledStateMachine
| Method | Description |
|---|---|
Init(name, dmType, stateCount) |
Initialize the state machine |
Start() |
Start the state machine |
Send(event) |
Send an event |
IsInState(stateID) |
Check if state is active |
Finished |
Whether machine has terminated |
RaiseInternal(name) |
Raise an internal event |
SendToTarget(target, event) |
Send event to target |
Event
| Property | Description |
|---|---|
Name |
Event name |
Type |
EventTypeExternal, EventTypeInternal, or EventTypePlatform |
SendID |
Send ID for delayed events |
Origin |
Origin URI |
OriginType |
Origin type |
InvokeID |
Invoke ID for child events |
Data |
Event data map |
RawData |
Raw event data |
| Factory Method | Description |
|---|---|
NewEvent(name) |
Simple event |
Builder() |
Fluent builder |
RuntimeContext
| Method | Description |
|---|---|
EnqueueInternal(event) |
Add internal event to queue |
EnqueueExternal(event) |
Add external event to queue |
DequeueInternal() |
Get next internal event |
DequeueExternal() |
Get next external event |
ScheduleDelayedSend(...) |
Schedule a delayed send |
CancelDelayedSend(sendID) |
Cancel a delayed send |
ContinuousExecutor
| Method | Description |
|---|---|
NewContinuousExecutor(sm) |
Create executor for state machine |
Start() |
Start goroutine; registers autonomous wakeup listener if machine implements IRuntimeAwareStateMachine |
Stop() |
Stop executor and deregister wakeup listener |
Send(name string) |
Send named event and wait for processing (blocking) |
SendEvent(event *Event) |
Send event and wait for processing (blocking) |
SendAsync(name string) |
Send named event asynchronously (non-blocking) |
SendEventAsync(event *Event) |
Send event asynchronously (non-blocking) |
OnEvent(fn func(*Event)) |
Set callback invoked after each event is processed |
OnFinished(fn func()) |
Set callback invoked when machine reaches a final state |
Wait() |
Block until machine finishes or Stop() is called |
IsRunning() bool |
Return true if event loop is active |
RunToCompletionExecutor
| Method | Description |
|---|---|
NewRunToCompletionExecutor(sm) |
Create synchronous executor |
Send(name string) |
Send named event synchronously (blocking) |
SendEvent(event *Event) |
Process event synchronously on caller's goroutine |
OnEvent(fn func(*Event)) |
Set event callback |
OnFinished(fn func()) |
Set completion callback |
IsFinished() bool |
Check if state machine finished |
Tracing
| Type | Description |
|---|---|
ITraceListener |
Base interface for trace listeners |
IInvokeAwareTraceListener |
Extended interface with invoke context |
JsonlTraceWriter |
JSONL trace file output |
ConsoleTraceListener |
Console debugging output |
TraceAdapter |
Base implementation for selective overrides |
Event Introspection
all := machine.GetAllEvents() // []string
enabled := machine.GetEnabledEvents() // []string, guard-aware
forState := machine.GetEventsForState("s1") // []string
enabledFor := machine.GetEnabledEventsForState("s1") // []string
Example: Tracing to JSONL
package main
import "example.com/mymachine/scxmlgen" // or "github.com/scxmlgen/scxmlgen"
func main() {
// Create trace writer
trace, _ := scxmlgen.NewJsonlTraceWriter("trace.jsonl")
defer trace.Close()
// Create and configure state machine
machine := NewMyStateMachine()
machine.SetTraceListener(trace)
// Start - trace will record all state changes
machine.Start()
machine.Send(scxmlgen.NewEvent("go"))
}