Human-in-the-Loop AI Approval Flows
Design the approval cards that let an agent pause safely before it sends, changes, spends, or deletes anything on a user’s behalf.
Interview answer
Persist each consequential AI action as a state machine. Show the target and parameters, let the user approve, reject, or safely edit, then distinguish approval from server-verified execution and completion.
“The agent can do it for you” sounds great until the action affects a customer, a repository, or a bank account. The right answer is not to remove automation; it is to give automation a clean handoff point where the user can see, edit, approve, or reject the proposed action.
A good approval flow is a real state machine, not a modal taped onto a chat message. It must survive refreshes, avoid double execution, communicate what is still pending, and preserve a useful record of what the user chose.
01Choose actions that need approval
Read-only actions such as searching documentation can run automatically. Actions with side effects should generally pause: sending external messages, publishing, deleting, changing access, creating paid resources, or modifying code outside a sandbox.
The risk is contextual. Sending a draft to yourself may be low risk; sending a contract to a customer is not. Design an action policy on the server and send the frontend a clear requiresApproval state rather than making the decision from button labels.
02The approval card must answer four questions
Suppose the agent drafts an email to a customer. A useful card says: “Send email to alex@example.com,” shows the subject and full body, and labels the action Pending approval. The user can edit the draft or reject it. A generic “Continue” button would hide the very choice they are being asked to make.
Before a user can responsibly approve an action, the card should make four facts obvious:
- What will happen? “Send an email” is not enough; show the operation in plain language.
- Who or what is affected? Show recipient, repository, record, environment, or destination.
- What are the meaningful parameters? Let users inspect and, where safe, edit the subject, message, branch, amount, or query.
- What is the current state? Pending approval, executing, completed, rejected, expired, or failed must be explicit.
03Model the action as a durable state machine
The message stream can stop or a tab can refresh, so do not keep approval state only in React state. Persist an action id and its transition history. The frontend renders that record and sends a decision tied to its id.
type ApprovalStatus =
| 'pending_approval'
| 'approved'
| 'rejected'
| 'executing'
| 'completed'
| 'failed'
| 'expired';
A useful transition is pending_approval → approved → executing → completed. Rejection and expiry end the pending branch; execution can fail. Disable duplicate decisions once the action is moving. After approval, show “Executing…” until the server confirms completion. “Approved” is not the same as “the email was sent.”
04Editing is powerful, so scope it tightly
Allow edits only for values your policy explicitly supports. If a user changes a recipient or an amount, the server should revalidate the edited payload and may need to create a new approval version. Never mutate a previously approved request invisibly.
For high-risk actions, show a diff between the original proposal and the edited version. It is easier for a user to approve a change when they can see exactly what moved.
05Give the card a server-owned contract
The browser can render an action record such as {"id":"act_42","version":3,"status":"pending_approval","type":"send_email","recipient":"alex@example.com"}. It should send a decision with that id and version, not send a new free-form email instruction to the model. If another tab already rejected version 3, the server returns a conflict and the client refreshes the card instead of applying a stale approval.
Keep the two operations distinct: decide records the user's choice, while execute performs the side effect under server policy. The server may perform them in one transaction-backed workflow, but the UI still needs to represent both stages. If the card is edited, create a new version and ask the user to approve the exact edited payload. This prevents an old approval from silently authorizing changed parameters.
06Make retries idempotent and outcomes auditable
Picture this failure: the user clicks Approve, the server sends the email, but the network drops before the browser receives the response. If the UI simply shows the Approve button again, a second click could send a duplicate. Persist the action id and decision key, then fetch the server's current action state on reconnect. A retry of the same decision must resolve to the same result; the server must enforce that atomically.
Store who approved, which version of the draft they saw, when execution started, and the final provider result. If the provider outcome is unknown, show “Checking status” instead of confidently saying “Failed” or offering a new send immediately.
Walk through a refresh after approval, a double-click, an edited recipient, and a lost success response. If the card still tells the truth in those cases, it is a real workflow rather than a decorative confirmation modal.
Key Takeaways
- 01Require approval for side effects, not for every low-risk read operation.
- 02An approval card clearly explains the action, affected target, meaningful parameters, and status.
- 03Persist approval state so it survives refreshes and stream interruptions.
- 04Approval, execution, success, and failure are separate states; do not collapse them into one button click.
- 05Use idempotency keys and an audit trail so retries cannot trigger a duplicate side effect.