Zephyr Hierarchical State Machines: Verify Entry and Exit Actions at Transition Boundaries
A hierarchical state machine is correct only when its actions match the transition boundary. Moving between sibling states should not tear down their shared parent’s resource; leaving that parent should. In Zephyr SMF, verify the action sequence directly rather than checking only the final state pointer. A machine can arrive at the right state after performing the wrong cleanup.
This guide pins upstream Zephyr 3.7.0 SMF with CONFIG_SMF_ANCESTOR_SUPPORT=y and CONFIG_SMF_INITIAL_TRANSITION=y. An executed host fixture compiled the unmodified SMF source and header with small type, configuration-macro and logging shims. It verifies sequential framework behavior, not a Zephyr event loop, interrupt integration or target timing.
Make hierarchy an ownership diagram
Consider a hypothetical controller with parent P containing children A and B, and a separate parent Q containing child C. P owns a communication session; A and B represent different activities within that session. Q owns a different operating mode. Entry and exit actions should follow those ownership boundaries.
The 3.7.0 SMF guide explains hierarchical support, optional initial-child transitions and the least-common-ancestor rule. For a sibling transition A to B, P remains active. For B to C, the machine leaves B and P before entering Q and C. A self-transition is different: its state’s exit and entry actions execute.

Prepare a transition ledger
For every event, record the active leaf, callback that handles the event, destination leaf or parent, expected exits, expected entries and resources that must remain live. Initial-child transitions should be explicit in the state table. With initial transitions disabled, do not pass a parent as a destination and assume the framework can choose its child.
The tagged state definitions make the callback order concrete: parent entry precedes child entry, while child exit precedes parent exit. The application object must place struct smf_ctx first. Keep one serialized execution owner for a context; SMF is not an event queue or an automatic synchronization layer for several callers.
| Fixture transition | Expected boundary actions | Resource interpretation |
|---|---|---|
| Initialize P, initial child A | Enter P, enter A | Establish parent before child |
| A to B | Exit A, enter B | Keep P’s session |
| B to C | Exit B, exit P, enter Q, enter C | Replace parent-owned mode |
| A to A | Exit A, enter A | Reinitialize A’s own state |
| C to P with initial child A | Exit C, exit Q, enter P, enter A | Reestablish the configured child path |
Control event propagation deliberately
A child run action gets the first opportunity to handle an event, after which parent run actions may execute. Call smf_set_handled() when the child has handled the event without transitioning and the parent must not process it again. A transition already stops that propagation. This distinction matters when both child and parent have a fallback response to the same input.
Do not assume that calling smf_set_state() returns from your C callback. Any statements following the call remain ordinary application code. Return explicitly if they would otherwise operate on resources just released by the transition. Likewise, prepare transition-related application data before the destination entry action needs it; the transition is performed during the call rather than scheduled for a later loop iteration.
Keep transition requests out of exit callbacks
The tagged implementation rejects a state-change request made during an exit action. An exit callback should perform the cleanup associated with the transition already in progress, not attempt to redirect it. If cleanup failure requires a different policy, represent that decision in the surrounding application protocol instead of relying on an ignored nested transition.
There is also a useful source/documentation boundary in this release: a header comment describes a null destination as valid, but the implementation logs an error and returns when new_state is null. Do not use null as a universal “exit everything” operation. For this pinned release, use an explicit state or the documented termination API according to the desired lifecycle.
smf_set_terminate() supplies a nonzero termination result. The application must stop calling smf_run_state() when that result is returned. Do not equate termination with automatically executing every desired exit action; make cleanup obligations explicit and test the path that performs them.
Executed action-sequence checks
The original host fixture recorded short action tokens into a bounded string and compared complete sequences. GCC 14.2.0 compiled the upstream source with C11, optimization enabled and warnings treated as errors. Both hierarchy and initial-transition configuration flags were enabled; logging shims counted rejected requests rather than implementing Zephyr logging.
Nine checks passed: initial parent/child entry, child-to-parent run propagation, handled-event suppression, self-transition, sibling transition, cross-parent transition with an attempted exit-time redirect, return through a parent’s initial child, null-destination rejection and explicit termination. In the cross-parent case, the attempted redirect did not change the expected B/P exit and Q/C entry sequence.
These results qualify the tested sequential action ordering. They do not prove application resource correctness or a hardware response deadline. Repeat the same ledger with real callbacks and replace irreversible hardware actions with controllable test doubles where possible. Track resource acquisition and release counts alongside action tokens.
Finally, exercise duplicate, unexpected and delayed events. Verify that each event is consumed once by the intended level, that sibling transitions preserve parent resources, and that termination leaves the application in its defined final condition. A state-machine review is complete when both the state path and its ownership effects are explained.