Human approval for one exact AI agent action
Approve the message that will actually be sent
Your support agent drafts a reply about a delayed booking. You want a person to check the recipient and wording before the message goes out. A general instruction to ask first leaves the sending credential available. An approval of the plan is also incomplete if the agent can change the request after the person says yes.
An API key tells the API which app is calling. It does not record a person's decision about this message. A rate limit permits fewer sends, including the wrong one. A log records what happened after delivery. You need to hold the call before it reaches the messaging API, then connect the approval to the call that resumes.
Mark the action as needing approval
Register the message route on the app first. From your backend, grant a run bound to the customer. Mark the sending action with approval. Keep the admin key on the backend; it must never enter the agent's context.
curl -X POST https://gate.odilelabs.com/api/projects/PROJECT/runs \
-H "Authorization: Bearer $ADMIN" -H 'content-type: application/json' \
-d '{"app":"APP_ID","scope":{"customer":"A"},"ttl_seconds":600,
"actions":[{"rule":"POST /customers/{customer}/messages","approval":true}]}'
The first matching call returns 202 and x-odile-outcome: approval_required. It has not been sent upstream. Gate returns an action_id. The held path and body are encrypted so the approver can inspect the exact call. They are kept until the action runs, is rejected or expires, for at most 24 hours and never beyond the run.
import { Gate } from 'https://gate.odilelabs.com/sdk/gate.mjs';
const gate = new Gate({ gateway, appKey, runToken: token });
const held = await gate.request('POST', '/customers/A/messages', {
body: { text: 'We are reviewing your booking.' }
});
if (held.outcome === 'approval_required') {
console.log(held.actionId); // Show the waiting action to your operator.
const result = await gate.waitForApproval(held);
console.log(result.outcome);
}
A person decides. The agent resumes.
The person opens the dashboard with an approver key and reviews the recipient path, method and body. They approve or reject that action. The demo script plays this role with its admin key and says so. A backend can also submit the decision through the approval endpoint:
curl -X POST https://gate.odilelabs.com/api/projects/PROJECT/actions/ACTION_ID/approve \
-H "Authorization: Bearer $APPROVER" -H 'content-type: application/json' -d '{}'
Approval does not send the request. The SDK polls its state and resumes after approval, preserving the original body bytes and idempotency key and adding x-odile-action. A changed method, path, query or body gets 409 approval_mismatch. A second execution attempt gets already_executed. Rejection and expiry end the wait without sending the message. Do not treat an unknown outcome as permission to send a new message.
Know what was checked
Gate checks method, path and query, not body contents. Human review must assess the message itself. Your API still needs ownership checks and row-level security. Only calls through Gate are protected; keep direct credentials away from the agent. See OWASP's excessive agency guidance and the Gate Actions reference.
Try it
Try it: watch an altered message fail and the approved message appear once.
curl -s https://gate.odilelabs.com/actions-quickstart.sh | sh