Skip to Content
ConceptsApproval flow
Open in Claude Open in ChatGPT

Approval flow

When you grant a scope with require_approval: true, the agent can still attempt every call. AgentValet just doesn’t let those calls reach the platform until you say yes.

This is the full path from “agent tried to do something” to “the thing actually happened” (or didn’t).

Step 1 — The agent calls; AgentValet queues

The agent signs and sends its request normally. AgentValet checks the signature, looks up the permission, sees require_approval: true, and instead of forwarding the call to the platform, it:

  • Inserts a row into the approval_queue with the action, the agent, the platform, the scope, and the original request payload
  • Returns to the agent: { approval_id, status: "pending", expires_at, created_at }
  • Notifies you

The agent doesn’t get an answer to its API call — it gets a receipt. Approval requests expire after 10 minutes by default. After that, the queued row is marked expired and you can’t act on it.

Step 2 — You get notified

You are notified on every plan: by push where your plan and device allow it, and by email when push can’t reach you. Delegates are emailed as well. The pending approval is also waiting for you in the dashboard.

Web Push notification (immediate, Team and above). If you’ve enabled push notifications and granted the browser permission, you get a notification on whatever device has the AgentValet dashboard open: which agent, which platform, which action. Click it to land on the approval card.

Email to you, when push can’t reach you (every plan). If no push reaches one of your devices, because your plan doesn’t include push or you haven’t turned it on, you get the request by email instead. Its one-click link expires with the request, 10 minutes by default.

Email magic-link to delegates (Team and above; up to 3 active delegates on Team). Org admins manage delegates on the Settings page. Active delegates receive an email at the same time:

  • From: AgentValet <[email protected]>
  • Subject: Approval required [{short approval id}]: {agent} → {platform} {METHOD}
  • Body: plain-text summary plus a single-use magic link that expires in 24 hours

Push notifications use Web Push (works on desktop Chrome, Android Chrome, and any browser with service-worker support). Native APNs / FCM are not wired up yet.

A pending approval card in the dashboard
A pending approval as it appears in the dashboard.

Step 3 — Someone decides

There are three places you can act on a pending approval:

  1. The dashboard at /approvals. Click approve or deny.
  2. The mobile view at /m/approvals/:id. If you’ve registered a passkey, you can confirm with biometric (Touch ID / Face ID / Windows Hello) instead of typing in a session. The passkey is optional: a plain Approve still works.
  3. The magic link in the delegate email. Opens a page showing the action, plus an approve/deny button. The link is single-use — once it’s been clicked through, the same link from the same email won’t work again (we hash the JWT and store the hash on the row).

For passkey-based approval, AgentValet uses WebAuthn. The challenge expires in 5 minutes; the credential is registered against your owner record and verified on every use, including a clone-detection counter.

Who can approve

An approver is one of:

  • An admin of the organisation the request belongs to.
  • An active approval delegate, when your plan includes delegation. Team allows 3 active delegates and Enterprise allows 999.

Members cannot approve. The approver’s role is read at the moment they decide, on every path: the dashboard, a passkey, the one-click link and a push notification. A member who tries on the dashboard or with a passkey gets 403 admin_only. The one-click link answers with the specific refusal code.

Delegates decide only through the link in their email. Org admins add, switch off and remove delegates on the Settings page, and every change is written to the audit log: delegate.added, delegate.deactivated, delegate.activated, delegate.updated (with before and after values) and delegate.removed, each naming the admin who made it. The audit export returns these rows.

The first valid decision wins. A later attempt on a request that has already been decided gets 409. An expired or cancelled request gets 410.

Removed and deleted approvers

If someone has been removed from the organisation, or their account has been deleted, they cannot approve on any path. The one-click link and passkey finish answer 403 approver_deleted; on the dashboard a removed member is refused because their sign-in no longer belongs to the organisation. The refusal is recorded in the audit log as approval.decision_refused. The same event also records self_approval refusals.

When a member is removed or their account is deleted, AgentValet also:

  • removes their passkeys;
  • switches off any delegate entries that use their email;
  • records owner.approver_authority_revoked in the audit log.

Require a different approver

Admins can turn on Require a different approver in Settings.

  • When it is on, nobody can approve a held request from an agent they own, on any path. A delegate whose email matches the owner counts as the owner. The refusal is 403 self_approval. This blocks approving only: the agent’s owner can always deny their own agent’s held request.
  • It can only be switched on when at least 2 people can approve. Otherwise the request fails with 409 insufficient_approvers.
  • If the number of eligible approvers later drops below 2, the setting stays on. The Settings page shows a warning, and requests that nobody can approve expire unapproved. The audit log records approval.no_eligible_approver.
  • Turning the setting on or off is recorded as org.approval_settings_changed.
  • Every approve or deny receipt carries distinct_approver_required.

Refusal codes

CodeMessage
admin_onlyOnly an organisation admin can approve or deny this request.
not_in_orgYou are not an approver for the organisation this request belongs to.
delegate_inactiveYour approval delegation has been turned off.
delegation_not_on_planApproval delegation is not included in this organisation’s plan.
approver_deletedYour access to this organisation has been removed.
self_approvalThis organisation requires someone other than the agent’s owner to approve.
no_eligible_approverNo one in this organisation can approve this request right now.

If AgentValet cannot check who may approve, it answers 503 decision_context_unavailable. Nothing was decided. Try again.

Step 4 — The action runs (or doesn’t)

If you approve, AgentValet does not tell the agent and wait — it re-executes the original request itself. The same payload that came in originally is now forwarded to the platform, the response is captured, and the queue row is updated with result_json (or execution_error on failure) and executed_at.

Your approval covers the call you saw, not access the agent has since lost. Before anything reaches the platform, the replay re-checks that:

  • the request is still exactly the one you were shown: method, endpoint, body and forwarded headers are re-hashed and compared with the commitment signed into the hold and decision receipts (payload_commitment_mismatch; see Payload commitment);
  • it still lands where you approved it: the same connection, the account pinned when it was held (if one was) and the person it was evaluated for (request_hash_mismatch);
  • the agent is still active (and, for a child agent, that it and its parent still are, with the scope still in the parent’s grant);
  • the grant has not expired (grant_expired);
  • a grant still covers the call’s connection and is not suspended (grant_revoked, platform_suspended);
  • the requested scope is still in that grant (scope_revoked);
  • the endpoint still matches the scope, and the recipient, repo and account restrictions and the external-send setting still allow the call.

A failed check is recorded as the execution_error with a signed audit receipt, and nothing is sent. Narrowing, suspending or deleting a grant also expires any pending approval it no longer covers, straight away, so you are not asked to approve a call that can no longer run.

If you deny, the queue row is updated with the denial. No platform call is made.

What approvers see

The approval screens read the same stored request that the payload commitment covers, but they present it rather than print it. Values that look like personal data are masked (“redacted”), lists of objects are summarised as a count, the payload table starts collapsed unless the call sends outside your organisation, and forwarded headers are not shown. The one-click email and push page shows the agent, platform, scope and endpoint only, not the body. The signed commitment proves which payload you approved and which one ran; it does not prove every field was on screen. For a high-value action, open it in the dashboard and expand the payload before approving.

Either way, the decision is recorded in the audit log along with who actioned it (or which delegate’s magic link was used).

Step 5 — The agent finds out

While all this is happening, the agent has been polling.

  • Endpoint: GET /v1/approvals/:id
  • Returns: { status, result?, execution_error?, expires_at, executed_at }
  • Rate limit: 60 polls/minute per agent

When status flips to approved and executed_at is set, the response includes the captured platform response — the agent gets what it would have gotten on the original call, just delayed by however long it took you to decide. On denied or expired, the agent gets the status and can tell its user.

Why this design

A few things are deliberate:

  • The agent doesn’t sign the approval. You do. An agent that’s been compromised can’t approve its own destructive call by holding a different key.
  • Approvals are short-lived. 10 minutes for the queue entry, 24 hours for the delegate email. The longer an approval sits open, the more likely the context behind it has gone stale.
  • Re-execution happens on the proxy. The agent never gets a “you may now do this” green light — AgentValet does the call. The agent only sees the result.

Next

Last updated on