States and Transitions
Source of truth:QTG v3 uses a two-layer state model: RequestState (the full request lifecycle) and NodeState (the progress state of each execution node). The request state is derived from the states of its nodes.src/qtg/domain/states.pyState derivation:src/qtg/application/services/advance.py
Table of Contents
- RequestState Enum
- NodeState Enum
- REQUEST_TRANSITIONS - request-state transition graph
- NODE_TRANSITIONS - node-state transition graph
- State transition diagrams
- Terminal vs Recoverable states
- State Derivation — derive_request_state()
- apply_request_state_from_nodes() - side effects
- Effect of the has_side_effect flag
- Frontier advancement - advance_after_completion()
RequestState Enum
RequestState tracks the full lifecycle of a movement request. It is based on enum.StrEnum and stored in the DB column movement_requests.request_state.
NodeState Enum
NodeState is the execution progress state of an individual node in the plan graph. It is stored in the DB column movement_request_nodes.node_state.
REQUEST_TRANSITIONS
REQUEST_TRANSITIONS is the allowed transition graph of type Mapping[RequestState, set[RequestState]]. The can_transition_request_state() function checks this dictionary to decide whether a transition is allowed.
Main transition paths
NODE_TRANSITIONS
Main transition paths
State transition diagrams
RequestState transition diagram
NodeState transition diagram
NodeState linear path (happy path)
Terminal vs Recoverable states
Terminal states (no further transitions)
Terminal RequestState values:COMPLETED, FAILED, REJECTED, EXPIRED, CANCELLED
- They have an empty
set()inREQUEST_TRANSITIONS. - When a terminal state is reached, reservation handling (
consumed/released) and callback delivery run.
COMPLETED, FAILED, CANCELLED, SKIPPED
- These states have no transition targets.
Recoverable states
NodeState.UNKNOWN is the only recoverable node state.
SUBMITTING->UNKNOWN: submit result is unclear because of a network timeout, etc.OBSERVING->UNKNOWN: RPC failure during observe
UNKNOWN nodes are handled by the recover_unknown_nodes() worker:
- It calls
executor.recover(context)to check the result. - It transitions to
SUBMITTED(recovery succeeded) orFAILED(deterministic failure), depending on the result.
RequestState.MANUAL_INTERVENTION is a recoverable request-level state:
- The operator can resume it to
EXECUTING, or force it toFAILED/COMPLETED.
RequestState.WAITING_MANUAL_ACTION is the waiting state for a manual gate:
- It returns to
EXECUTINGafter the manual gate is resolved.
State Derivation
derive_request_state() checks the states of all nodes in a request and derives the request state. The system does not set RequestState directly from outside. It decides it from the set of node states.
derive_request_state() logic
Decision order (highest priority first)
Step 1: all-complete checkCOMPLETED or SKIPPED, it returns COMPLETED immediately.
Step 2: classify failed/unknown nodes
MANUAL_INTERVENTION vs FAILED
- If there is a failed/unknown node and there is also a completed side-effect node ->
MANUAL_INTERVENTION - If there is a failed node and there is no completed side-effect node ->
FAILED - If there are only unknown nodes (no failed nodes), and there is no completed side-effect node ->
None(keep the current state)
- If there is a
READYnode whose name starts with themanual_prefix, - and there are no currently active running nodes,
- then ->
WAITING_MANUAL_ACTION
Summary table of derived results
apply_request_state_from_nodes()
This function writes the result ofderive_request_state() to the DB and handles side effects (callback, reservation).
Side effects by state
The release condition for a reservation is decided by
should_release_reservation():
REJECTED,EXPIRED: always releaseCANCELLED,FAILED: release only when there is no completed side-effect node
Effect of the has_side_effect flag
has_side_effect is defined on MovementPlanNode.has_side_effect and copied to MovementRequestNode.has_side_effect.
Meaning
True: the node’s executor can change external state (withdrawal submit, on-chain tx send, etc.)False: read-only or observe-only (deposit observe, finality check, etc.)
Role in state derivation
If a node with a side effect is alreadyCOMPLETED and another node fails:
- the funds may already have moved, so the system cannot treat it as a simple
FAILED; - it transitions to
MANUAL_INTERVENTIONand requires operator judgment.
withdrawal_actioncompleted,deposit_observefailed ->MANUAL_INTERVENTION- the withdrawal completed, but deposit confirmation failed - manual confirmation is required
withdrawal_actionfailed ->FAILED- the withdrawal itself failed - the system can fail safely
Role in reservation release
If there is a completed side-effect node, the system does not release the reservation even onFAILED/CANCELLED. The funds may already be in transit, so the hardcap quota must stay reserved.
Frontier Advancement
advance_after_completion() changes successor nodes to READY after a node completes and updates the frontier.
Frontier advancement order
- From the completed node’s
plan_node, look up successors alongon_successedges. - Change successor nodes that are in
BLOCKEDstate toREADY. - Update
request.current_frontierwith the list of successor keys. - Send the
frontier_advancedcallback. - If there is no successor (end of the graph), call
apply_request_state_from_nodes()to decide request termination.
State transition validation functions
State transitions can be checked for validity through the validation functions:set_node_state() and set_request_state(). Each transition creates a MovementEvent audit record.
Cross-References
- types-and-enums.md — graph-construction types such as NodeKind and EdgeType
- error-taxonomy.md — error categories and NodeState transition mapping
- ../executors/overview.md — executor lifecycle and state transitions
- ../workers/runtime-workers.md — dispatch/observe/recover workers
- ../infrastructure/data-model.md — MovementRequest and MovementRequestNode tables