Sequences and Stages
Building state machines with Arc's sequence and stage constructs
Sequences are Arc’s way of building multi-step procedures. A sequence contains ordered stages, with only one stage active at a time. When you enter a stage, its flows execute; when you leave, they stop.
This is the most common use case for Arc: test sequences, startup routines, shutdown procedures, and any workflow that progresses through distinct phases.
Everything in a stage runs at the same time. The code looks sequential, but Arc executes all flows in a stage concurrently.
A Simple Example
Here’s a basic pressurization sequence:
sequence main {
stage idle {
0 -> valve_cmd,
start_btn => next
}
stage pressurize {
1 -> valve_cmd,
tank_pressure > 500 => next
}
stage hold {
1 -> valve_cmd,
wait{duration=30s} => next
}
stage complete {
0 -> valve_cmd
}
}
start_btn => main This sequence:
- Starts in
idle: Valve is closed (0), waiting for the operator to press start - Moves to
pressurize: Opens the valve (1), waits for pressure to reach 500 - Moves to
hold: Keeps valve open for 30 seconds - Ends in
complete: Closes the valve
The start_btn => main at the bottom is the entry point. When start_btn receives
a truthy value (non-zero), the sequence starts.
Concurrency Within Stages
All flows in a stage run simultaneously. This is different from traditional line-by-line code execution.
stage pressurize {
// ALL of these run at the same time:
1 -> valve_cmd, // keep valve open
tank_pressure -> pressure_display, // update display
tank_pressure > 600 => abort, // safety limit
tank_temp > 300 => abort, // temperature limit
abort_btn => abort, // operator abort
tank_pressure > 500 => next // success condition
} You don’t write loops to “keep checking” these conditions. Arc monitors all of them concurrently while the stage is active.
Line order determines priority for transitions. When multiple one-shot edges
(=>) could fire in the same cycle, the one listed first wins. Always put safety
conditions before success conditions.
Transitions
Use => (one-shot edge) to transition between stages:
stage idle {
start_btn => next // advance to next stage in order
}
stage running {
stop_btn => idle // jump to specific stage
emergency => abort // jump to different sequence
} Transition Targets
Using next on the last stage is a compile error.
Entry Points
Sequences start when triggered by a channel. Wire a channel to a sequence using =>:
start_cmd => main
emergency_stop => abort The sequence starts when the source channel has a truthy value (non-zero).
Triggering from Schematics
Entry points are typically u8 virtual channels:
- Create a
u8virtual channel in Synnax (e.g.,start_cmd) - Add a button to a schematic that writes to
start_cmd - When clicked, the button writes
1to the channel - Arc sees the truthy value and starts the sequence
Stage Entry Semantics
When entering a stage:
- All one-shot transition states reset (can fire again)
- All stateful variables in the stage reset to initial values
- Reactive flows start fresh
Stages don’t remember their previous state. If you transition away and come back, everything starts over.
Complete Example: Test Sequence
Here’s a realistic test stand sequence with safety handling:
sequence main {
stage idle {
0 -> press_valve,
0 -> vent_valve,
start_btn => next
}
stage pressurize {
1 -> press_valve,
0 -> vent_valve,
// Safety conditions (listed first = highest priority)
tank_pressure > 600 => abort,
tank_temp > 300 => abort,
abort_btn => abort,
// Success condition
tank_pressure > 500 => next
}
stage hold {
1 -> press_valve,
tank_pressure > 600 => abort,
abort_btn => abort,
wait{duration=30s} => next
}
stage depressurize {
0 -> press_valve,
1 -> vent_valve,
tank_pressure < 50 => complete
}
stage complete {
0 -> press_valve,
0 -> vent_valve
}
}
sequence abort {
stage safed {
0 -> press_valve,
1 -> vent_valve,
0 -> igniter
}
}
start_btn => main
emergency_stop => abort Notice that safety conditions are listed before success conditions in each stage. The
abort sequence can be triggered from any stage or from the emergency_stop button.
Transitioning to abort leaves the main sequence entirely.
Direct Assignments
Inside stages, you can use both flow syntax and direct assignment:
stage pressurize {
// Flow syntax: reactive pipeline
sensor -> filter{} -> display,
// Direct assignment: equivalent to "1 -> valve_cmd"
valve_cmd = 1
} Direct assignments behave like constant flows. They continuously set the value while the stage is active. Use whichever is clearer for your use case.