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 runs its items in order: each item finishes before the next begins.
The most common item is a stage, a block of flows that run in parallel and stay live until a transition fires. A sequence is usually a chain of stages: pressurize, hold, vent, complete. But a sequence can also contain simpler items (writes, waits, and condition gates) for the parts of a procedure that don’t need parallel monitoring.
This is the most common use case for Arc: test sequences, startup routines, shutdown procedures, and any workflow that progresses through distinct phases.
A Simple Example
Here’s a basic pressurize, hold, and vent sequence:
sequence main {
stage pressurize {
1 -> press_vlv_cmd
tank_pressure > 700 => abort // safety
tank_pressure > 500 => next // target reached
}
0 -> press_vlv_cmd
wait{10s}
stage vent {
1 -> vent_vlv_cmd
tank_pressure < 50 => next
}
0 -> vent_vlv_cmd
}
sequence abort {
stage safed {
0 -> press_vlv_cmd
1 -> vent_vlv_cmd
}
}
start_btn => main The main sequence has five items that run in order:
pressurizeis a stage that holdspress_vlv_cmdopen while watching two exits: an over-pressure abort at 700 psi and a success at 500 psi.0 -> press_vlv_cmdcloses the valve once the stage exits.wait{10s}dwells for ten seconds.ventis a stage that opens the vent valve and waits for pressure to drop below 50.0 -> vent_vlv_cmdcloses the vent valve.
The safety abort jumps to the separate abort sequence, which safes the valves. The
start_btn => main at the bottom is the entry point. When start_btn receives a
truthy value (non-zero), the sequence starts.
Parallel Monitoring
Everything in a stage runs at the same time. The code looks sequential, but Arc executes all flows in a stage concurrently.
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 conditional edges
(=>) are truthy in the same cycle, the one listed first wins. Always put safety
conditions before success conditions.
Transitions
A transition is a flow statement whose target is a stage or sequence name. When the edge fires, the current stage or sequence deactivates and control jumps to the target.
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
} Most transitions use =>, which fires while the source expression is truthy. For
test-stand conditions like pressure > 500, this means the transition fires the first
tick the condition holds. Transitioning to an already-active target is a no-op, so a
source that stays truthy will not re-enter a running sequence.
A -> flow into a stage or sequence name is also a transition. It fires on the first
value the source produces, regardless of truthiness. This is rare in practice.
Transition Targets
Using next when there is no next item in the enclosing sequence 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 receives a new truthy write (non-zero). A historical truthy channel value does not trigger the sequence.
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
Anonymous Sequences Start Automatically
A sequence declared without a name starts on its own when the program loads. Use this for procedures that should run once, immediately, without an operator trigger:
authority 200
sequence {
0 -> vent_vlv_cmd
1 -> press_vlv_cmd
wait{5s}
0 -> press_vlv_cmd
} A named sequence still needs a trigger to start.
Stage Entry Semantics
When entering a stage:
- All stateful variables in the stage reset to initial values
- Reactive flows start fresh
- Timing nodes (
wait,interval) reset their countdowns - Inline sub-sequences restart from their first item
- Channel triggers only react to new writes. Values written before stage entry are ignored
Stages don’t remember their previous state. If you transition away and come back, everything starts over.
This is important for resumable sequences. A common pattern is a sequence that parks
in a yield stage and waits for a fresh command to re-enter:
sequence controller {
stage run {
// ... control logic ...
stop_cmd => stop
}
stage stop {
0 -> valve_cmd
wait{duration=250ms} => yield
}
stage yield {
// Only a new write to start_cmd will re-enter run.
start_cmd => run
}
}
start_cmd => controller Sequences Without Stages
A sequence item doesn’t have to be a stage. For a straight-line procedure, the items can be writes, waits, and condition gates:
sequence prime {
0 -> vent_vlv_cmd
1 -> press_vlv_cmd
wait{5s}
tank_pressure > 500
0 -> press_vlv_cmd
}
start_btn => prime The sequence opens the press valve, waits five seconds, then waits until pressure reaches 500 psi before closing the valve. Each item completes before the next starts.
Sequence Item Types
A condition gate is any boolean expression on its own line. The sequence does not re-run earlier items while the gate holds. It just waits.
A bare sequence can’t respond to safety conditions while a gate is holding. If you need to watch for abort conditions during a step, put the step inside a stage.
Inline Stages in a Sequence
Use an inline stage when a step needs to watch multiple conditions at once, like a success check with a timeout backstop. An inline stage is a stage block written directly as a sequence item, with or without a name:
sequence press {
1 -> press_vlv_cmd
stage {
tank_pressure > 700 => abort // over-pressure safety
tank_pressure > 500 => next // success condition
wait{30s} => abort // timeout
}
0 -> press_vlv_cmd
} While the inline stage is live, all three transitions are armed in parallel. Whichever
fires first wins, and line order breaks ties. => next advances to the next sequence
item; => abort jumps to the abort sequence (defined elsewhere).
This is cleaner than breaking the procedure into three named stages when the extra structure exists only to hold one gate.
Inline Sequences in a Stage
The reverse nesting also works. An inline sequence inside a stage runs its items in order while the stage’s parallel flows continue:
sequence main {
stage fire {
// Safety monitoring runs the whole time the stage is live
chamber_temp > 2000 => abort
abort_btn => abort
// Ordered ignition sub-procedure
sequence {
1 -> igniter_cmd
wait{200ms}
1 -> ox_main_vlv_cmd
1 -> fuel_main_vlv_cmd
wait{100ms}
0 -> igniter_cmd
}
}
stage abort {
0 -> ox_main_vlv_cmd
0 -> fuel_main_vlv_cmd
0 -> igniter_cmd
}
}
start_cmd => main When fire activates, the inline sequence starts from its first item. It walks through
the ignition steps while the safety transitions watch in parallel. If a safety
transition fires, the stage exits and the inline sequence stops wherever it was.
If the stage re-enters later, the inline sequence restarts from its first item.
Use an inline sequence when a stage has a small ordered sub-procedure but still needs parallel safety monitoring.
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.