Skip to main content

Human-in-the-Loop Approval

Some actions are too risky to allow automatically and too useful to block outright: a large payout, a bulk delete, an email to a customer. A rule with action: 'require_approval' pauses the agent, asks a human, and records who decided what.

agent calls guard()
↓ a require_approval rule matches
Auditail creates an approval request ──▶ dashboard + your webhook + your callback
↓ (an approver clicks Approve or Deny)
guard() returns approved: true / false ◀── decision, who made it, and when

Add an approval rule​

import { Auditail } from 'auditail-sdk';

const auditail = new Auditail({ apiKey: process.env.AUDITAIL_API_KEY! });

auditail.addRule({
name: 'payout-over-limit',
description: 'Payouts over $10,000 need a human',
action: 'require_approval',
approvalTimeoutSeconds: 600, // optional; default 900 (15 min)
condition: (o) => (o.payload.amount as number) > 10_000,
});

const result = await auditail.guard({
action: 'create_payout',
payload: { amount: 25_000, beneficiary: 'new' },
agentId: 'payout-agent',
});

if (!result.approved) return result.reason; // "Denied by alice: unknown beneficiary"
await payouts.create(...); // only runs if a human said yes
from auditail import Auditail
from auditail.types import GuardrailRule

auditail = Auditail(api_key=os.environ["AUDITAIL_API_KEY"])
auditail.add_rule(GuardrailRule(
name="payout-over-limit",
description="Payouts over $10,000 need a human",
action="require_approval",
approval_timeout_seconds=600,
condition=lambda o: o.payload["amount"] > 10_000,
))

result = auditail.guard(action="create_payout", payload={"amount": 25_000}, agent_id="payout-agent")
if not result.approved:
return result.reason

guard() waits for the decision. The result tells you what happened:

FieldMeaning
approvedtrue only if a human approved it.
reasonHuman-readable outcome, including who decided and their comment.
approval.statusapproved, denied, expired, or unavailable.
approval.decidedBy / commentThe approver and their note.

Safe by default​

SituationResult
Approver clicks Approveapproved: true
Approver clicks Denyapproved: false
Nobody answers before the timeoutexpired, denied automatically
Auditail cannot be reachedunavailable, denied. An action that needs approval never proceeds without one.
A different rule blocks it firstBlocked, no approval is requested

Who can approve​

An agent's API key can never approve a request. Approvals are decided with a separate approver token (atl_appr_...), so a compromised or confused agent cannot approve itself. During the beta we issue approver tokens to you on request.

  • Sign in to https://app.auditail.io with an approver token to get Approve and Deny buttons.
  • Sign in with the agent key to see requests read-only.
  • The approver's name comes from the token, not from anything the caller sends. It is recorded by Auditail and cannot be forged by the agent.

Get notified where your team works​

Pending requests always appear in the dashboard. You can also be notified any way you like:

  1. Webhook: paste an HTTPS URL in the dashboard under Notifications. It works with Slack incoming webhooks, Microsoft Teams, Discord, Zapier, or your own endpoint.
  2. SDK callback: run your own code the moment a request is created, for example to post from your own Slack bot or send an email:
const auditail = new Auditail({
apiKey: process.env.AUDITAIL_API_KEY!,
onApprovalRequested: (a) => slack.post(`Approve ${a.action} for ${a.agentId}: https://app.auditail.io/#approval=${a.id}`),
});
auditail = Auditail(api_key=..., on_approval_requested=lambda a: notify(a["id"], a["action"]))

Webhook details​

Auditail sends POST requests with a JSON body for approval.requested and approval.decided (plus a test event from the dashboard button):

{
"type": "approval.requested",
"text": "Approval needed: agent \"payout-agent\" wants to run \"create_payout\" (rule payout-over-limit). Review: https://app.auditail.io/#approval=apr_...",
"content": "...same text, for Discord...",
"approveUrl": "https://app.auditail.io/#approval=apr_...",
"approval": { "id": "apr_...", "status": "pending", "agentId": "...", "action": "...", "payload": { }, "expiresAt": "..." }
}
  • text and content make Slack, Teams, Mattermost and Discord work with no code. Agent-supplied names are sanitized in this text (no mentions, markup or clickable links). The structured approval object carries the exact values for machines.
  • The URL must be https on a public hostname.
  • Every request is signed. Verify it so you only act on real notifications:
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody: string, headers: Record<string, string>, secret: string): boolean {
const expected = 'sha256=' + createHmac('sha256', secret)
.update(`${headers['x-auditail-timestamp']}.${rawBody}`).digest('hex');
const got = headers['x-auditail-signature'] ?? '';
return got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}

Also check that x-auditail-timestamp is recent (for example within 5 minutes) to reject replays. The signing secret is shown once when you save the webhook.


Don't block your request handler: wait: false​

By default guard() waits for the human, which can be minutes. In a web request or serverless function, return immediately and check later:

const result = await auditail.guard({ action: 'create_payout', payload, agentId: 'payout-agent', wait: false });
if (result.pending) {
await jobs.enqueue({ approvalId: result.approval!.id }); // result.approved is false for now
return { status: 'awaiting_approval' };
}

// later, in a worker:
const outcome = await auditail.waitForApproval(approvalId); // 'approved' | 'denied' | 'expired' | 'unavailable'
if (outcome.status === 'approved') await payouts.create(...);
result = auditail.guard(action="create_payout", payload=payload, agent_id="payout-agent", wait=False)
if result.pending:
queue.put(result.approval.id) # result.approved is False for now

outcome = auditail.wait_for_approval(approval_id) # .status == "approved" | "denied" | "expired" | "unavailable"

Treat anything other than approved as a no.


What goes in the audit log​

Auditail itself writes both steps into the tamper-evident log, so they are not claims made by the agent:

  • approval.requested: the action, the full (redacted) payload, the rule, and when the request expires.
  • approval.decided: the outcome (approved, denied, or expired), the approver, their comment, and the time.

Event names starting with approval. are reserved, and clients cannot write them. Secrets in the payload are redacted by the SDK before the request is sent, so approvers never see passwords or tokens.

Limits​

  • Timeout: 10 seconds to 24 hours per request (default 15 minutes).
  • At most 100 requests can be pending per account at once.
  • An expired request cannot be approved afterwards.