States & transitions
How a flow's state machine is built: states as the nodes, the start state, transitions that fire on events, the any_state fallback, and the on_timeout safety net.
A flow is a finite-state machine. Flows introduced the shape; this page covers the building blocks — states, the start state, transitions, the any_state_transitions fallback, and on_timeout. The next page, Actions and checks, covers what runs inside a transition.
States are the nodes
A state is a node in the FSM — a point where the flow waits for something to happen. Each UE running the flow sits in exactly one state at a time. A state declares two things, both optional:
transitions— event-keyed rules for what to do when a message (or synthetic event) arriveson_timeout— a fallback fired if nothing matches within a deadline
Name states for what the flow is waiting for. The shipped flows use wait_* prefixes for waiting states (wait_auth_request, wait_security_mode, wait_context_setup) and plain nouns for terminal states (registered, failed). The names are arbitrary labels — the engine only cares that every target resolves to a real state.
The start state
One state is the entry point, named at the top level by initial_state. Every UE begins there. How the FSM leaves the start state depends on the flow's type:
- Client flows initiate the procedure. The engine fires a synthetic
Startevent intoinitial_stateonce per UE, which dispatches the firstsend. A client flow must have a transition matchingStartatinitial_state. - Server flows wait for a peer to send the first message. There is no
Startevent; the FSM spawns when an inbound message matches a transition atinitial_state. A server flow must not have aStarttransition there, and must have at least one transition to receive that first message.
The engine enforces both rules at flow-load time, so a mismatched type fails fast rather than hanging at runtime.
Final states
The states named in the top-level final_states list are terminal. Reaching one ends the flow for that UE — the engine records the outcome and stops dispatching events to that UE. Final states carry no transitions; they are leaves of the graph.
Most flows have exactly two: one success state and one failure state. Negative tests often invert the labels — a "rejected" outcome is the success of the test. The names are conventional, not magic; the engine treats any state in final_states as terminal.
Transitions — the edges
A transition is an edge out of a state. It binds an event to a target state and an optional ordered list of actions. When the FSM is in state S and an event arrives, the engine scans S's transitions in order and fires the first whose event matches: its actions run, then the FSM advances to target.
If no transition in S matches, the FSM stays in S and waits for the next event (or the timeout). Events that match nothing are simply ignored — there is no implicit error.
A transition can target any state — a later waiting state, a final state, or (for retry and polling patterns) back to the same state. The exact field tables for transitions and events live in the flow schema reference; this page stays conceptual.
Three event forms
The event key takes one of three shapes.
Simple — a single event name. Most transitions look like this.
OR-compound — fires when any one of the listed events arrives. Useful when an AMF may skip authentication for an already-authenticated UE and jump straight to security mode.
AND-compound — fires only after every listed event has arrived. The engine accumulates the events it has seen so far for the current state and re-checks the AND clause on each new arrival, firing once the set is complete.
Synthetic events
Most events name a wire message the protocol resolver emitted. Four events have no wire frame — the engine raises them itself.
| Event | When it fires |
|---|---|
Start | Once per UE for client flows, into initial_state |
Error | On a decode error or an unrecoverable failure on the inbound path |
StateTimeout | When an on_timeout deadline elapses (you wire this through on_timeout, not by naming it in event:) |
UplaneComplete | After an uplane_start action finishes its traffic run |
The common use of a synthetic event in authored YAML is Error in any_state_transitions, to land every error path in failed.
any_state_transitions — the global fallback
Some events can arrive in any state and always mean the same thing — a protocol-level Error, an ErrorIndication, an abort. Rather than repeating the same transition in every state, declare it once at the top level under any_state_transitions.
The engine checks state-specific transitions first; only if none matched does it fall back to any_state_transitions. A state that handles an event locally overrides the global rule.
An any-state transition may target _self to fire its actions and stay in the current state — useful for handling a recurring keep-alive or status message that can arrive anywhere without advancing the procedure.
on_timeout — the safety net
Every waiting state should carry an on_timeout. Without one, a peer that never replies leaves the UE parked in that state until the engine's outer flow deadline trips — the flow then fails without a defined outcome instead of landing in a state you chose.
The deadline starts when the FSM enters the state. duration is a Go-style duration string — 10s, 2m, 500ms. When it elapses, the engine raises a StateTimeout event and moves the UE to target. The target is any state name: usually failed, but for retry patterns it can point back at a waiting state.
An on_timeout may carry its own actions, which run before the target is applied — the same execution semantics as a transition's actions. This expresses "wait N, then do X" without needing an inbound event to drive it, for example a token-expiry probe that re-sends after an idle delay.
on_timeout on every wait_* state, and reach for any_state_transitions for Error. Between the two, every UE has a defined exit no matter what the peer does — which is what keeps a 10,000-UE load run from accumulating stuck sessions.Putting it together
Here is the full graph of the shipped gnb/registration.yaml flow — start state, waiting states, transitions on inbound NAS messages, a timeout out of every waiting state, and an any-state Error edge into failed.
Every node, edge, and label in that diagram is a few lines of YAML. The procedure walks from idle through three waits to registered; any timeout or error diverts to failed. That is the whole model: states are where you wait, transitions are how you move, and the timeout and any-state fallbacks guarantee you always move somewhere.
Where to go next
- Actions and checks — what runs inside a transition:
check,extract,send, and more - Flow schema reference — the exact field tables for states, transitions, events, and timeouts