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