Approvals are human-in-the-loop gates on jobs. When a queue has approvals enabled, every job enqueued to it pauses and waits for a human to approve or reject before the callback handler is called. The job payload is visible during review so you can make an informed decision.

Use approvals when:

  • An AI agent is generating outbound emails and a human should review before send
  • A financial transaction above a threshold requires sign-off
  • Sensitive data access (e.g., exporting a contact list) needs audit-trail approval
  • An automated process touches production systems and you want a manual gate

How the flow works

  1. Enable approvals on the queue

    Set approval_required: true when creating or updating the queue.

  2. Enqueue a job

    Your code enqueues a job normally. The job enters a pending_approval state instead of waiting.

  3. Rotor creates an approval record

    An approval_record row is created with status: "pending" and an apv_<32hex> ID.

  4. Webhook fires

    If you've configured a webhook, Rotor sends an approval.pending event to your endpoint with the approval ID, job ID, queue name, and payload.

  5. Human approves or rejects

    Via CLI, SDK, REST API, or the MCP approve_job tool in Claude Code.

  6. Job proceeds or fails

    On approval, the job moves to waiting and your callback handler runs. On rejection, the job is marked failed with the rejection reason.

Enable approvals on a queue

Create a new queue with approvals

import { Rotor } from "@rotorsh/sdk";
 
const rotor = new Rotor({ apiKey: process.env.ROTOR_API_KEY! });
 
await rotor.queues.create({
  name: "ai-outreach",
  callback_url: "https://your-app.example.com/rotor/ai-outreach",
  approval_required: true,
  defaultJobOptions: { attempts: 3 },
});

Enable approvals on an existing queue

await rotor.queues.update("ai-outreach", {
  approval_required: true,
});

Listing pending approvals

CLI

rotor approvals list --status pending

SDK

const { approvals } = await rotor.approvals.list({
  status: "pending",
  limit: 50,
});
 
for (const apv of approvals) {
  console.log(apv.id);          // apv_<32hex>
  console.log(apv.jobId);       // the job waiting for approval
  console.log(apv.queueName);   // which queue
  console.log(apv.payload);     // the job payload — inspect before deciding
  console.log(apv.createdAt);   // when the approval record was created
}

Filter by queue

const { approvals } = await rotor.approvals.list({
  status: "pending",
  queue: "ai-outreach",
});

Get a single approval

const apv = await rotor.approvals.get("apv_abc123...");
console.log(apv.payload); // inspect the job payload

Approving and rejecting

CLI

# Approve
rotor approvals approve apv_abc123...
 
# Reject
rotor approvals reject apv_abc123...

SDK

// Approve — job proceeds to your callback handler
await rotor.approvals.approve("apv_abc123...");
 
// Reject — job is marked failed
await rotor.approvals.reject("apv_abc123...");

REST API

# Approve
curl -X POST "https://api.rotor.sh/v1/approvals/apv_abc123.../approve" \
  -H "Authorization: Bearer $ROTOR_API_KEY"
 
# Reject
curl -X POST "https://api.rotor.sh/v1/approvals/apv_abc123.../reject" \
  -H "Authorization: Bearer $ROTOR_API_KEY"

The approval.pending webhook

If you configure a webhook endpoint, Rotor sends a POST to it when an approval record is created. The payload type is ApprovalPendingData.

// Webhook payload shape
type ApprovalPendingData = {
  approvalId: string;   // apv_<32hex>
  jobId: string;        // the waiting job
  queueName: string;    // which queue
  payload: unknown;     // the job payload — safe to log and display
  workspaceId: string;  // your workspace
};

Example payload:

{
  "event": "approval.pending",
  "data": {
    "approvalId": "apv_a1b2c3d4...",
    "jobId": "job_e5f6g7h8...",
    "queueName": "ai-outreach",
    "payload": {
      "contactId": "cid_123",
      "subject": "Quick question about your stack",
      "body": "Hi Sarah, ..."
    },
    "workspaceId": "ws_abc123"
  }
}

Use this webhook to post a Slack message to your team, create a Notion entry, or trigger any other notification flow. Your webhook handler can embed a direct link to the approval using the approvalId.

Tip

You can approve and reject directly from a Slack message by posting the approvalId back to the Rotor API via a Slack shortcut or button. The MCP approve_job tool makes this even easier from Claude Code.

Using the MCP approve_job tool

If you use the Rotor MCP server, Claude Code gains an approve_job tool. From your terminal you can list pending approvals and approve or reject them without leaving your editor.

# In Claude Code
> List pending approvals for the ai-outreach queue
> Approve apv_abc123...
> Reject apv_xyz789... — the subject line is too aggressive

This is the fastest review path for Claude Code-using teams: see the payload, make a call, continue building.

See MCP quickstart to connect the MCP server.

What happens after rejection

A rejected job is marked failed with reason: "rejected". It does not retry — rejection is terminal. The job appears in your failed jobs list with the rejection timestamp.

If you need to re-attempt a rejected job, enqueue a new job with the corrected payload.

Warning

Approvals pause the job indefinitely — there is no automatic timeout. If an approval sits pending for a long time, the job will not run until someone acts. Build a notification flow (webhook → Slack, email) so approvals don't silently stall.

Filtering by status

The status filter accepts: pending, approved, rejected.

// All approved approvals for audit purposes
const { approvals } = await rotor.approvals.list({
  status: "approved",
  queue: "ai-outreach",
  limit: 100,
});

Use cursor for pagination:

const page1 = await rotor.approvals.list({ status: "pending", limit: 20 });
const page2 = await rotor.approvals.list({
  status: "pending",
  limit: 20,
  cursor: page1.nextCursor,
});

Next steps