Handoffs API

Handoffs enable structured agent-to-agent communication. One agent profile can send a message to another, optionally referencing a source task and requesting approval before the recipient acts. Handoffs power delegation chains, collaborative workflows, and human-in-the-loop review gates.

Quick Start

Create a handoff from one agent to another, list pending handoffs, and resolve one:

// 1. Create a handoff — analyst delegates review to a reviewer agent
interface Handoff {
id: string;
status: string;
toProfileId: string;
requiresApproval: boolean;
}

const handoff: Handoff = await fetch('/api/handoffs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  fromProfileId: 'analyst-agent',
  toProfileId: 'reviewer-agent',
  sourceTaskId: 'task-9d4e-a1b2',
  subject: 'Q4 revenue analysis ready for review',
  body: 'Analysis is complete with 3 charts and an executive summary. Please verify the methodology and sign off.',
  priority: 1,
  requiresApproval: true,
}),
}).then((r: Response) => r.json());
// -> { id: "hoff-c7a1...", status: "pending", requiresApproval: true, ... }

// 2. List all pending handoffs for the reviewer
const pending: Handoff[] = await fetch('/api/handoffs?status=pending&profileId=reviewer-agent')
.then((r: Response) => r.json());
console.log(`${pending.length} handoffs awaiting review`);

// 3. Resolve the handoff (recipient marks it as completed)
//    Note: status updates happen through the handoff bus internally.
//    The reviewer agent processes the handoff and its status transitions
//    from pending -> accepted -> in_progress -> completed automatically.
console.log(`Handoff ${handoff.id} is being processed by ${handoff.toProfileId}`);

Base URL

/api/handoffs

Endpoints

List Handoffs

GET /api/handoffs

Retrieve handoff messages with optional filtering by status and recipient profile. Results are ordered newest first, limited to 100 entries.

Query Parameters

Param Type Req Description
status enum Filter by message status: pending, accepted, in_progress, completed, rejected, expired
profileId string Filter by recipient profile UUID

Response 200 — Array of handoff message objects

Handoff Message Object

FieldTypeReqDescription
idstring (UUID)*Message identifier
fromProfileIdstring*Sender agent profile ID
toProfileIdstring*Recipient agent profile ID
taskIdstring (UUID)Source task that triggered the handoff
targetTaskIdstring (UUID)Task created or assigned by the handoff
subjectstring*Message subject line
bodystring*Message body text
attachmentsstring (JSON)Serialized JSON attachments
prioritynumber (0–3)*Priority level (default 2)
statusenum*Lifecycle state: pending, accepted, in_progress, completed, rejected, expired
requiresApprovalboolean*Whether the handoff needs approval before execution
approvedBystringProfile ID that approved the handoff
parentMessageIdstring (UUID)Parent message for threaded handoff chains
chainDepthnumber*Depth in the handoff chain (0 = root)
createdAtISO 8601*Creation timestamp
respondedAtISO 8601When the recipient responded
expiresAtISO 8601Expiration timestamp

Fetch all pending handoffs for a specific recipient — useful for building an inbox or approval queue:

// Fetch pending handoffs for the reviewer agent
interface Handoff {
id: string;
priority: number;
subject: string;
fromProfileId: string;
}

const handoffs: Handoff[] = await fetch('/api/handoffs?status=pending&profileId=reviewer-agent')
.then((r: Response) => r.json());

console.log(`${handoffs.length} pending handoffs`);
handoffs.forEach((h: Handoff) => {
console.log(`  [${h.priority}] ${h.subject} (from: ${h.fromProfileId})`);
});

Example response:

[
  {
    "id": "hoff-c7a1-b3d4",
    "fromProfileId": "analyst-agent",
    "toProfileId": "reviewer-agent",
    "taskId": "task-9d4e-a1b2",
    "subject": "Q4 revenue analysis ready for review",
    "body": "Analysis is complete with 3 charts and an executive summary. Please verify the methodology and sign off.",
    "priority": 1,
    "status": "pending",
    "requiresApproval": true,
    "chainDepth": 0,
    "createdAt": "2026-04-03T10:45:00.000Z",
    "expiresAt": "2026-04-04T10:45:00.000Z"
  }
]

Create Handoff

POST /api/handoffs

Send a handoff message from one agent profile to another. The message is dispatched through the handoff bus and persisted to the database.

Request Body

FieldTypeReqDescription
fromProfileIdstring*Sender agent profile ID
toProfileIdstring*Recipient agent profile ID
sourceTaskIdstringSource task UUID that triggered this handoff
subjectstring*Message subject line
bodystring*Message body text
prioritynumberPriority level 0–3 (default 2)
requiresApprovalbooleanRequire approval before recipient acts (default false)
parentMessageIdstringParent message ID for threaded chains

Response 201 Created — The created handoff message object

Errors: 400 — Missing required fields or handoff bus error

Send a handoff requesting approval — the recipient must accept before acting on it:

// Create a handoff with approval required
interface HandoffResponse {
id: string;
status: string;
}

const handoff: HandoffResponse = await fetch('/api/handoffs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  fromProfileId: 'analyst-agent',
  toProfileId: 'reviewer-agent',
  sourceTaskId: 'task-9d4e-a1b2',
  subject: 'Analysis complete — ready for review',
  body: 'Q4 revenue analysis is done. 3 charts generated, executive summary attached. Please review methodology before sign-off.',
  priority: 1,
  requiresApproval: true,
}),
}).then((r: Response) => r.json());

console.log(handoff.id);     // "hoff-c7a1-..."
console.log(handoff.status); // "pending"

Example response:

{
  "id": "hoff-c7a1-b3d4",
  "fromProfileId": "analyst-agent",
  "toProfileId": "reviewer-agent",
  "taskId": "task-9d4e-a1b2",
  "subject": "Analysis complete — ready for review",
  "body": "Q4 revenue analysis is done. 3 charts generated, executive summary attached. Please review methodology before sign-off.",
  "priority": 1,
  "status": "pending",
  "requiresApproval": true,
  "chainDepth": 0,
  "createdAt": "2026-04-03T10:45:00.000Z"
}

Errors: 400 — Missing required fields or handoff bus error

Get Handoff

GET /api/handoffs/{id}

Retrieve a single handoff message by ID. Returns the full message object including approval metadata and response timestamp.

Response 200 — Handoff message object (same shape as the list endpoint)

Errors: 404 — Handoff not found

Fetch a single handoff to check whether a recipient has accepted or rejected it:

// Fetch a handoff by ID to check current status
interface Handoff {
id: string;
status: string;
approvedBy?: string;
respondedAt?: string;
}

const handoff: Handoff = await fetch("http://localhost:3000/api/handoffs/hoff-c7a1-b3d4")
.then((r: Response) => r.json());

console.log(`Status: ${handoff.status}`);
if (handoff.respondedAt) {
console.log(`Approved by ${handoff.approvedBy} at ${handoff.respondedAt}`);
}

Example response:

{
  "id": "hoff-c7a1-b3d4",
  "fromProfileId": "analyst-agent",
  "toProfileId": "reviewer-agent",
  "taskId": "task-9d4e-a1b2",
  "subject": "Analysis complete — ready for review",
  "body": "Q4 revenue analysis is done. 3 charts generated, executive summary attached. Please review methodology before sign-off.",
  "priority": 1,
  "status": "accepted",
  "requiresApproval": true,
  "approvedBy": "reviewer-agent",
  "chainDepth": 0,
  "createdAt": "2026-04-03T10:45:00.000Z",
  "respondedAt": "2026-04-03T11:02:00.000Z"
}

Approve or Reject Handoff

PATCH /api/handoffs/{id}

Approve or reject a pending handoff. The status transitions from pending to accepted (approve) or rejected. Records the approver identity and response timestamp.

Request Body

FieldTypeReqDescription
actionenum*approve or reject
approvedBystringIdentifier of the approver (defaults to "user")

Response 200 — The updated handoff message object

Errors:

  • 400 — Missing or invalid action
  • 404 — Handoff not found
  • 409 — Handoff is not in the pending state (already accepted, rejected, completed, or expired)

Approve a pending handoff on behalf of a supervising agent or human reviewer:

// Approve a pending handoff
interface Handoff {
id: string;
status: string;
approvedBy?: string;
}

const updated: Handoff = await fetch("http://localhost:3000/api/handoffs/hoff-c7a1-b3d4", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
  action: "approve",
  approvedBy: "sehgal.manav@gmail.com",
}),
}).then((r: Response) => r.json());

console.log(`Status: ${updated.status}`);  // "accepted"
console.log(`Approved by: ${updated.approvedBy}`);

Reject a handoff when the recipient cannot or should not take on the work:

// Reject a handoff — status transitions to "rejected"
await fetch("http://localhost:3000/api/handoffs/hoff-c7a1-b3d4", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "reject", approvedBy: "reviewer-agent" }),
});

Example response after approval:

{
  "id": "hoff-c7a1-b3d4",
  "fromProfileId": "analyst-agent",
  "toProfileId": "reviewer-agent",
  "subject": "Analysis complete — ready for review",
  "status": "accepted",
  "requiresApproval": true,
  "approvedBy": "sehgal.manav@gmail.com",
  "chainDepth": 0,
  "createdAt": "2026-04-03T10:45:00.000Z",
  "respondedAt": "2026-04-03T11:02:00.000Z"
}

Status Lifecycle

StatusDescription
pendingMessage sent, awaiting recipient action
acceptedRecipient acknowledged the handoff
in_progressRecipient is actively working on the handoff
completedHandoff fulfilled successfully
rejectedRecipient declined the handoff
expiredHandoff timed out before a response