Series: hitl-agentic-systems · Part 4
AI / Agentic Systems
States and State Machine for HITL
States: pending_review, approved, rejected, timeout; simple state machine; reviewed_by, reviewed_at.
2026-03-095 min read
Post 4 in the HITL in Agentic Systems series. Post 3 defined HITL and the approval gate. This post covers state and a simple state machine for one gate: pending_review, approved, rejected, timeout; plus reviewed_by and reviewed_at for audit.
Opening: From gate to state
An approval gate is not just “stop and wait for a human” — the system must know the state of each item: waiting for review (pending), approved, rejected, or timed out. Each time the human clicks Approve/Reject or on timeout, state changes; we also store who reviewed and when for audit and debug. This post describes the state machine for one gate and the fields to persist.
1. Basic states
Value: You will use at least four main states: pending_review, approved, rejected, timeout. (You can add modified if you need to distinguish “edited and waiting for approve again”.) These are the basis for DB and workflow engine design.
Challenges: Multiple gates in one flow will have more states (e.g. gate_1_approved, gate_2_pending). This post keeps it simple: one gate, four states. Post 5 onward covers queue and multiple items; post 6 covers identity and audit.
Design: State table and meaning:
| State | Meaning |
|---|---|
| pending_review | Item is waiting for human review; workflow is stopped at the gate. |
| approved | Human clicked Approve; workflow may run the next step. |
| rejected | Human clicked Reject; workflow rollbacks or stops. |
| timeout | No decision within the allowed time; handle per policy (e.g. auto reject or escalate). |
State transitions (events):
- pending_review + human_approve → approved
- pending_review + human_reject → rejected
- pending_review + timeout → timeout (or rejected)
Solution: The minimal state machine for one gate has these four states and three events. When transitioning to approved or rejected, we also record reviewed_by (user_id) and reviewed_at (timestamp). For timeout, reviewed_by can be null or system_id per policy.
Implementation: In code you can use an enum (PendingReview, Approved, Rejected, Timeout) and a transition table (from_state, event, to_state). A diagram (circles and arrows) helps the team share the same model. The next post will show a full single-gate diagram.
2. State machine for one gate
Value: You get one state-machine diagram for a simple flow: agent creates item → pending_review → human decides → approved / rejected / timeout. After approved, the workflow continues; on rejected or timeout, rollback or handle per policy.
Challenges: Multiple gates (e.g. content approval then deploy approval) yield more complex state; post 4 uses a single gate for clarity. Later posts (queue, audit) extend the model.
Design: Flow:
- created (optional, can be merged into pending_review): Agent finishes the item and pushes it to the gate.
- pending_review: Item waits in the queue; human sees it and clicks Approve / Reject, or does nothing until timeout.
- approved → workflow engine runs the next step (e.g. send email, deploy).
- rejected → workflow rollbacks or cancels; you can store a reject reason (optional field).
- timeout → can map to “rejected” or “escalate” per policy.
Transition table (from_state, event, to_state):
| from_state | event | to_state |
|---|---|---|
| pending_review | human_approve | approved |
| pending_review | human_reject | rejected |
| pending_review | timeout | timeout |
Solution: A full diagram (circle per state, arrows for events) in docs or wiki helps dev and product align. Example DB: table approval_items with columns id, workflow_id, state, reviewed_by, reviewed_at, created_at.
Implementation: In code or workflow config (BPMN, YAML, or code) ensure: (1) on entering the gate, create a record with state = pending_review; (2) on event from UI/API, update state and reviewed_by, reviewed_at; (3) when state = approved, trigger the next step; when rejected/timeout, trigger rollback or the right handler. Post 6 covers who may approve (identity, permissions) and the audit trail.
3. Metadata: reviewed_by, reviewed_at
Value: Whenever we transition from pending_review to approved or rejected, we store reviewed_by (reviewer identity) and reviewed_at (timestamp). This is the minimal audit trail: who reviewed, when — needed for compliance and debug (“Who approved that deploy at 3am?”).
Challenges: Identity (who may review which type, where user_id comes from) is covered in post 6. Here we only need the fields and meaning: when state changes, set these two fields. For timeout you can leave reviewed_by = null or use system_id (system-initiated transition).
Design: Suggested schema (per item):
id,workflow_id,step_id(gate identifier)state: enum pending_review | approved | rejected | timeoutreviewed_by: user_id (string or FK) or null if timeoutreviewed_at: timestamp or nullcreated_at,updated_at
On human_approve or human_reject: update state, set reviewed_by = current_user_id, reviewed_at = now(). On timeout: update state; reviewed_by can be null.
Solution: Always store reviewed_by and reviewed_at when state goes from pending_review to approved/rejected. For timeout you can set reviewed_by = null and reviewed_at = time of timeout. Next: post 5 (queue — where the human sees items and clicks), post 6 (identity, permissions, full audit trail).
Implementation: In the API or event handler: on approve/reject, check state = pending_review, update state and metadata, log if needed. Do not overwrite reviewed_by/reviewed_at after the item is already approved/rejected (immutable after decision). Post ends with a lead-in to post 5 (queue and approval channels) and post 6 (identity and audit).
Previous: What Is Human-in-the-Loop? (post 3). Next: Review Queue and Approval Channels (post 5).
