CitationBenchTalk to Sales
Concepts

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_APPROVAL and produces an Approval record
  • 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 step
  • every_outside_action — pause at publish + outreach + indexing
  • publish_or_outreach — narrower
  • none — 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

ConceptRelationship
AgentApprovals are part of the agent lifecycle; agents pause and resume
Eval GatesConditional rules that decide when to pause (not just whether)
WorkspacesWorkspace-level default policy
Link Building · inboundHeavy user — most inbound responses pause for review
Production · publishPublishing 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: true

The agent only pauses for human review when the rule matches.

On this page