Approval Workflows: gate agent actions without losing speed
How CitationBench pauses any agent step for human approval — durable, async, policy-layered — so agencies can run autonomous workflows that still earn client trust.
Approval workflows are how you keep autonomous agents from doing things you don't want them to do — without making everything synchronous. Any agent step that touches the outside world (publishing, sending outreach, submitting URLs to Google, negotiating with partners) can be gated: the agent pauses at the gate, surfaces what it's about to do, and waits for a human decision.
This is the single most important reason agencies trust CitationBench to run their client work.
The short version
- Any agent step can declare
requiresApproval: true - When triggered, the invocation moves to
WAITING_APPROVALand produces anApprovalrecord - Humans approve, reject, or approve-with-edits — via dashboard, Slack, webhook into a client portal, or API
- Three policy layers: workspace defaults → skill defaults → per-invocation overrides
- Eval Gates add conditional rules on top — escalate only when X is true
Why we modeled it this way
Three design constraints shaped this:
1. Agencies sell trust, not just velocity. A client paying $5k–20k/month for SEO needs to feel control over what gets published in their name. Speed without trust is a non-starter. Approval gates let agencies operate fast internally and still surface every consequential action for client sign-off.
2. Approval is async by default. A synchronous "wait for human" pattern means the agent process has to stay hot. CitationBench's approvals are durable — the invocation persists in WAITING_APPROVAL state for as long as it takes (hours, days, even weeks), and resumes cleanly when the human acts.
3. The same agent should work autonomously OR gated. Toggling approval shouldn't require code changes. So approval.required is a runtime flag on every invocation, plus a default at the skill, workspace, and agency levels. Same code paths in both modes.
The lifecycle
RUNNING
↓ agent reaches a step with requiresApproval
WAITING_APPROVAL
↓ approver decides
├── APPROVED → RUNNING (step executes)
├── APPROVED w/EDITS → RUNNING (step executes with the human's edits)
├── REJECTED → FAILED or skips the step (skill defines)
└── timeout → CANCELLED (configurable)What a paused invocation surfaces
{
"invocationId": "inv_***",
"status": "WAITING_APPROVAL",
"currentStep": {
"name": "send_outreach_email",
"approvalId": "appr_***",
"preview": {
"to": "marina@engineering-blog.com",
"subject": "Section 7 of your PM tools roundup",
"body": "Hi Marina, ...",
"scheduledAt": "2026-05-25T09:00:00-04:00"
},
"reason": "Outbound email — workspace policy requires approval",
"raisedAt": "2026-05-24T08:14:32Z",
"timeoutAt": "2026-05-31T08:14:32Z"
}
}Every approval surface — dashboard, Slack message, webhook — renders the preview. The human sees what would have been sent if they did nothing.
The three policy layers
1. Skill default
Each skill declares which of its steps require approval by default. bootstrap_brand defaults to approval on publish only. link_hunter defaults to approval on send_email. You can read the defaults via GET /v1/agent/skills/{slug}.
2. Workspace policy
A workspace can tighten or loosen the defaults across all skills:
"settings": {
"approvalPolicy": {
"publish": "auto",
"outreach_send": "require_approval",
"indexing_submit": "auto",
"every_outside_action": false
}
}3. Per-invocation override
When you call agent.invoke:
{
"skill": "content_factory",
"input": { ... },
"approval": {
"required": true,
"scope": "every_step"
}
}scope values:
every_step— pause at every stepevery_outside_action— pause at publish + outreach + indexingpublish_or_outreach— narrowernone— full autonomy (ignores workspace + skill defaults; requires explicit opt-in)
Approval actions
Approve as-is
curl -X POST .../v1/agent/invocations/inv_***/approve \
-H "Authorization: Bearer sk_live_***"Approve with edits
curl -X POST .../v1/agent/invocations/inv_***/approve \
-d '{
"edits": {
"subject": "Quick thought on your PM roundup",
"body": "Hi Marina, loved your roundup. Quick note on section 7 ..."
},
"note": "Softened opener"
}'The agent uses the edited values when it continues.
Reject
curl -X POST .../v1/agent/invocations/inv_***/reject \
-d '{
"note": "Wrong target — please skip this contact",
"skipStep": true
}'If skipStep: true, the skill may continue past the rejected step. Otherwise, the invocation transitions to FAILED.
Reject + rerun the step with different config
curl -X POST .../v1/agent/invocations/inv_***/reject \
-d '{
"rerun": {
"step": "drafts",
"config": { "tone": "more skeptical" }
},
"note": "Re-draft with a more skeptical tone"
}'Get pending approvals
curl -G .../v1/agent/approvals \
--data-urlencode "scope=workspace"{
"approvals": [
{
"approvalId": "appr_***",
"invocationId": "inv_***",
"skill": "content_factory",
"step": "publish",
"preview": { ... },
"raisedAt": "2026-05-24T08:14:32Z",
"timeoutAt": "2026-05-31T08:14:32Z"
}
]
}Notification surfaces
When a step pauses, CitationBench fires the agent.invocation.awaiting_approval webhook. Wire it into:
- Slack (post to a channel, button-based approve/reject)
- Email digest (daily summary for the approver)
- Your own client portal (render the preview, capture decisions)
- PagerDuty / opsgenie (for time-sensitive workflows)
How it interacts with other concepts
| Concept | Relationship |
|---|---|
| Agent | Approvals are part of the agent lifecycle; agents pause and resume |
| Eval Gates | Conditional rules that decide when to pause (not just whether) |
| Workspaces | Workspace-level default policy |
| Link Building · inbound | Heavy user — most inbound responses pause for review |
| Production · publish | Publishing to external CMSes is gated by default in agency workspaces |
Common patterns
1. Auto everything internally, approve everything client-facing
"approvalPolicy": {
"publish": "require_approval",
"outreach_send": "require_approval",
"indexing_submit": "auto"
}The default we recommend for agency workspaces.
2. Approve-once, auto-after
Useful for trusted relationships:
# First time you approve a send to this contact, mark "trusted"
curl -X POST .../v1/agent/invocations/inv_***/approve \
-d '{ "trustHenceforth": "contact" }'Future sends to the same contact auto-approve.
3. Approval queues for client portals
The webhook target is a small service that renders a preview to the client. The client clicks approve/reject. The service POSTs back to /v1/agent/invocations/{id}/approve or /reject.
4. Time-limited approvals
"approval": {
"required": true,
"timeoutMinutes": 240,
"onTimeout": "CANCEL"
}After 4 hours with no decision, the invocation cancels — useful for time-sensitive outreach (e.g. responding to a hot inbound within the same business day).
5. Eval-gate-driven approvals
You may not want every step gated — only the dangerous ones. Eval Gates wrap approvals in conditional rules:
- name: payment_request_escalation
applies_to: link_building.inbound
when:
llm_check: "Is the sender asking for money?"
then:
escalate_to_approval: trueThe agent only pauses for human review when the rule matches.
Related
- API: Agent · invoke — invocation lifecycle, approval endpoints
- API: Agent · approval — full CRUD
- Concept: Agent
- Concept: Eval Gates
- Concept: Workspaces
- Playbook: Client-approval gated publishing
Link Building
CitationBench models link building as a CRM — shared accounts, per-workspace relationships, typed events — so every outreach interaction carries years of context.
Eval Gates
Eval gates combine deterministic and LLM-evaluated conditions to let an agent run autonomously on 95% of cases and escalate only the 5% that need a human.