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
approvedBystring—Profile 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 8601—When the recipient responded
expiresAtISO 8601—Expiration 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
sourceTaskIdstring—Source task UUID that triggered this handoff
subjectstring*Message subject line
bodystring*Message body text
prioritynumber—Priority level 0–3 (default 2)
requiresApprovalboolean—Require approval before recipient acts (default false)
parentMessageIdstring—Parent 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
approvedBystring—Identifier 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: "[email protected]",
}),
}).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": "[email protected]",
  "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