CitationBenchTalk to Sales
Playbooks

Client-approval gated publishing for agency SEO ops

Gate every publish and outbound action behind explicit client approval — drafts move at agency speed, publishes wait for client trust, all powered by durable approval primitives.

Drafts move at agency speed. Publishes happen at client trust speed. Every publish (and every outbound action) pauses for the client to approve, render in their portal, edit if needed, and click go.

OutcomeEvery publish + outreach action gated by the client's explicit approval
TimeFree; uses existing approval primitives
CostFree; only the underlying invocation costs apply
PrereqsAn approval surface (Slack bot, client portal, or email) that can POST to CitationBench's approval endpoints

What it does

agent (any skill that publishes / sends / submits)
  ↓ reaches the outside-world step
  ↓ workspace policy: require_approval
  ↓ invocation moves to WAITING_APPROVAL
  ↓ approval.created webhook fires

your portal / Slack renders the preview
↓ client clicks approve / approve-with-edits / reject
↓ POST to /v1/agent/invocations/{id}/approve or /reject
  ↓ invocation resumes; publish actually fires

Step 1 — Set the workspace policy

curl -X PATCH .../v1/workspaces/ws_acme -d '{
  "settings": {
    "approvalPolicy": {
      "publish":         "require_approval",
      "outreach_send":   "require_approval",
      "indexing_submit": "auto"
    }
  }
}'

This is the per-workspace default. Individual invocations can tighten further but not loosen.

Step 2 — Wire the approval webhook

curl -X POST .../v1/webhooks -d '{
  "url":    "https://hooks.our-portal.com/approvals",
  "events": ["agent.invocation.awaiting_approval"]
}'

Webhook payload:

{
  "approvalId": "appr_***",
  "invocationId": "inv_***",
  "skill": "produce.publish",
  "step": "publish_to_wordpress",
  "workspace": "ws_acme",
  "preview": {
    "platform": "wordpress",
    "title": "How engineering teams track capacity",
    "slug": "engineering-team-capacity-tracking",
    "url": "https://acme.com/blog/engineering-team-capacity-tracking",
    "categories": ["Engineering"],
    "featuredImageUrl": "https://cdn.citationbench.com/.../hero.png",
    "scheduledAt": "2026-05-25T09:00:00-04:00"
  },
  "raisedAt": "2026-05-24T08:14:32Z",
  "timeoutAt": "2026-05-31T08:14:32Z"
}

Step 3 — Build the client-facing preview

Your portal renders the preview payload to the client with three actions: Approve, Edit + Approve, Reject. On click, POST back:

# Approve
curl -X POST .../v1/agent/invocations/inv_***/approve \
  -H "Authorization: Bearer sk_live_***" \
  -d '{ "approvalId": "appr_***", "note": "Looks good, ship it." }'

# Approve with edits
curl -X POST .../v1/agent/invocations/inv_***/approve \
  -d '{
    "approvalId": "appr_***",
    "edits":      { "title": "How engineering teams actually track capacity" },
    "note":       "Tightened title"
  }'

# Reject
curl -X POST .../v1/agent/invocations/inv_***/reject \
  -d '{ "approvalId": "appr_***", "note": "Wrong angle — rewrite with X" }'

Step 4 — Slack as the approval surface (fast path)

If you don't have a portal yet:

curl -X POST .../v1/integrations/slack -d '{
  "channel": "#acme-approvals",
  "approvalRouting": "post-and-buttons"
}'

Approvals post to Slack with Approve / Reject buttons. Click → the integration calls the API for you.

Step 5 — Time-limited approvals

For time-sensitive workflows (responding to inbound within the business day):

curl -X POST .../v1/agent/invoke -d '{
  "skill": "content_factory",
  "input": { "...": "..." },
  "approval": {
    "required":       true,
    "timeoutMinutes": 240,
    "onTimeout":      "CANCEL"
  }
}'

After 4 hours with no decision, the invocation cancels.

Step 6 — Trust-once patterns

Approve a particular send "and trust henceforth":

curl -X POST .../v1/agent/invocations/inv_***/approve -d '{
  "approvalId":      "appr_***",
  "trustHenceforth": "contact"
}'

Future actions to the same contact auto-approve. Useful for established partners.

One-shot setup

#!/usr/bin/env bash
set -euo pipefail
KEY="${CITATIONBENCH_API_KEY:?}"
WS="${WORKSPACE_ID:?}"
PORTAL="${PORTAL_HOOK_URL:?}"
BASE="https://api.citationbench.com/v1"

# 1. Tighten the workspace
curl -sf -X PATCH $BASE/workspaces/$WS \
  -H "Authorization: Bearer $KEY" \
  -d '{
    "settings": {
      "approvalPolicy": {
        "publish":         "require_approval",
        "outreach_send":   "require_approval",
        "indexing_submit": "auto"
      }
    }
  }'

# 2. Wire the webhook
curl -sf -X POST $BASE/webhooks \
  -H "Authorization: Bearer $KEY" \
  -d "{ \"url\": \"$PORTAL\", \"events\": [\"agent.invocation.awaiting_approval\"] }"

echo "Client-approval gating active for $WS. All publishes + outreach require explicit approval."

Gotchas

  • Auto-fire indexing. Indexing submissions are typically auto even when publish is gated — there's no risk to the client there.
  • Approval queue overload. If your client doesn't review for a week, the queue fills up. Set a timeoutMinutes to prevent stale work.
  • Trust-once is per resource type. Trusting contact doesn't trust account; trusting step doesn't trust the whole skill. Each is its own scope.
  • Don't override per-invocation when workspace says approve. The none scope on individual invocations only works if workspace policy permits it. Otherwise the policy wins.
  • Slack approvals are public to the channel. Use private channels or DMs for client-sensitive previews.

On this page