Suppose you approve an agent's support reply, then its recipient changes before the message is sent. The words you reviewed may be identical, but the action is different. A safe approval workflow has to preserve what you approved through the pause, restart and eventual execution.
Ask for approval when an action exceeds existing permission, needs human judgment or falls outside a defined automatic policy. Bind that decision to the exact recipient, payload and effect. When execution resumes, check the approved version, current permissions and whether the operation has already completed.
This guide shows how to build that action record and handle rejection, expiry, changed arguments and uncertain outcomes. The local approval example makes the pause-and-resume logic inspectable; it is a teaching fixture, not production authorization infrastructure.
Start with actions and existing authority
List the operations the agent can perform. For each one, identify the target system, data involved, possible effect and current authorization. A read operation can expose sensitive information; a write operation can sometimes be narrow and reversible. The tool name alone is insufficient.
The following matrix is an example policy to adapt, not a universal rule:
| Proposed action | Example treatment | Check before execution |
|---|---|---|
| Read an allowed internal article | Automatic within existing scope | Current user can access that document |
| Prepare an unsent support reply | Automatic draft | Draft stays inside the permitted workspace |
| Send that reply with an attachment | Approval when not already delegated | Recipient, message and exact attachment |
| Change a customer’s access | Restricted reviewer approval | Account, role change, reason and current state |
| Export records outside the organization | Deny unless the specific workflow authorizes it | Destination, allowed fields and eligible records |
Avoid asking again for work the user has already authorized within the same bounds. If every routine operation interrupts the user, the policy needs a more useful definition of scope.
Conversely, an earlier approval to draft something does not authorize sending every later version. Establish which changes remain within the original decision and which require another review.
Give the reviewer a concrete decision
An approval screen should answer: what will happen, to whom, using which data, and under whose authority?
For a support reply, show the exact recipient, subject, body and attachment identity. Name the destination account and the case that prompted the reply. Explain any material consequence in plain language. Give the reviewer an explicit approve or reject choice.
Behind that screen, retain a server-controlled record such as:
{
"action_id": "reply-1042",
"tenant_id": "example-team",
"type": "send_support_reply",
"recipient": "customer@example.invalid",
"body": "The exact message under review.",
"attachment_digest": "digest-of-the-reviewed-file",
"policy_version": "support-policy-v3",
"state": "pending"
}
This is an illustrative record, not an API contract. A real implementation also needs authenticated reviewer identity, timestamps, expiry rules and a stable representation of every material argument.
A digest can help detect changes to that representation. It does not prove permission or reviewer identity. The application must establish those separately; do not accept an “approved” field generated by the model as authorization.
Persist the pause
Store the action and decision outside the model conversation. Otherwise a browser refresh, a worker restart or a long delay can lose the relationship between the request and its approval.
Framework support can help with persistence. LangGraph documents interrupts that pause execution with saved state and resume using the same thread identifier. Its documentation also warns that the interrupted node restarts from its beginning on resume. That matters when code before the pause could produce an effect. LangGraph interrupts.
A useful application state sequence is:
pending → approved → executing → succeeded
→ rejected
→ expired
↘ uncertain
These are proposed application states, not mandatory framework names. Define the allowed transitions, which actor can make them and what happens after a crash. Treat an unknown external outcome as its own state instead of pretending it is either a clean failure or confirmed success.
Recheck the action when execution resumes
Approval and execution may be separated by hours. A reviewer can lose access, a record can change, or a destination can stop being valid during that interval.
Before acting, check that:
- The approval belongs to this action and organization.
- The decision came from an authorized reviewer through a trusted channel.
- The approved arguments still match the proposed operation.
- The approval is current under the workflow’s expiry policy.
- Current permissions and required record preconditions still hold.
- The operation has not already completed.
If the recipient or attachment changes, preserve the old decision and create a new reviewable version. Do not overwrite the approved payload and leave the approval attached.
Expiry is a policy choice. There is no single duration that fits every workflow. Choose it based on how quickly the relevant conditions can change, and show the reviewer when a fresh decision is required.
Protect the effect from replay
A repeated resume request should not send the same message twice. Neither should a worker retry after losing its connection.
Assign a stable operation key and use destination-side duplicate protection where available. AWS’s Builders’ Library explains why explicit caller-provided request identifiers are useful for expressing retry intent. A matching payload alone does not distinguish a retry from a deliberate second operation. Making retries safe with idempotent APIs.
A local “sent” flag cannot by itself solve a crash that happens after the destination accepts a message but before the flag is saved. Design recovery around the destination’s operation identifier, idempotency contract or an independent lookup of the resulting record.
If you cannot establish whether the action happened, keep it uncertain and investigate before repeating it. Report that state accurately to the user.
Test the decisions people actually make
Use a harmless destination stub first. The approval demonstration exercises a small persisted local decision record and duplicate protection without sending a message.
For your real workflow, retain these cases:
| Case | Expected behavior |
|---|---|
| Reviewer approves the displayed action | Only that action becomes eligible |
| Reviewer rejects | No effect occurs |
| Approval expires | Execution stops for a fresh decision |
| Payload changes after approval | The old decision cannot authorize the new version |
| Someone resumes twice | At most one logical effect is recorded |
| Wrong reviewer or organization | Decision is refused |
| Permission is revoked before execution | Current authorization stops the action |
| Destination accepts, then the connection drops | Recovery checks destination state before retrying |
The demonstration covers a subset of these application rules. It does not implement a production identity provider, a real external send or cross-system crash recovery. Passing a local fixture is a starting point for integration testing.
Put one boundary under control
Choose one consequential action in your agent. Write down its existing authority, the exact fields a reviewer must see, the stored decision and the recovery check after an uncertain result.
Then test the rejected and changed-payload cases before expanding automatic execution. Those cases reveal whether approval actually controls the action.
For help with permission boundaries, approval interfaces or resumption across integrations, review MCP & Agent Tooling and Workflow Engineering. Bring one redacted action example and its failure path so the discussion can stay concrete.
sinc-LLM