Agent Inbox: Direct Messaging

Give every agent a personal message queue. Send targeted handoffs, alerts, and task results directly to the agent that needs them — without broadcasting to everyone.

8 min read Tutorial 9 of 9 Intermediate

You've got four agents running in parallel — one building an API, one running tests, one applying migrations, one updating docs. Pub/sub is great for broadcasting "I'm done." But sometimes you need to tell one specific agent something private: "The schema changed. Update your types before running the next test suite."

That's the inbox.

What the Inbox Is

Every registered agent gets a personal message queue. Any caller can deliver a message; only the named recipient reads it. Messages are persistent — they wait in the inbox until read or cleared.

# pub/sub channel — everyone subscribed hears it
$ pd pub build:api '{"status":"done"}'

# agent inbox — only agent-bob receives it
$ pd inbox send agent-bob "Schema migration complete, ready for review"
Inbox vs pub/sub at a glance: pub/sub delivers to current subscribers in real-time (ephemeral). The inbox holds messages until the recipient explicitly reads or clears them (persistent). Use the inbox when the message matters even if the recipient isn't listening right now.

Registration is the Inbox Address

The inbox exists as long as the agent is registered. When you register an agent, you create its delivery address:

# Register yourself — this creates your inbox
$ pd agent register --agent my-agent \
    --purpose "Building the payment API"

# Now any caller can message you:
$ pd inbox send my-agent "Hey, the Stripe webhook secret changed"

Using the SDK:

const pd = new PortDaddy({ agentId: 'my-agent' });
await pd.register({ type: 'ci', purpose: 'Building the payment API' });
// Inbox is live at: POST /agents/my-agent/inbox

Sending Messages

CLI

Send from the terminal

$ pd inbox send agent-bob "Schema migration complete, ready for review"
API

Send via HTTP

curl -X POST http://localhost:9876/agents/agent-bob/inbox \
  -H 'Content-Type: application/json' \
  -d '{"content":"Schema migration complete","from":"agent-alice","type":"handoff"}'
SDK

Send from JavaScript

await pd.inboxSend('agent-bob', 'Schema migration complete', {
  from: 'agent-alice',
  type: 'handoff',
});

Message types are free-form strings. Common conventions:

Reading Your Inbox

CLI

# Read all messages
$ pd inbox
[09:42:15] [handoff] agent-alice: Schema migration complete, ready for review
[09:38:02] [alert]   system: Lock db-migrations held >30m

# Unread only (useful for polling scripts)
$ pd inbox --unread

SDK

// All messages
const { messages } = await pd.inboxList('my-agent');

// Unread only
const { messages } = await pd.inboxList('my-agent', { unreadOnly: true });

// Paginated
const { messages } = await pd.inboxList('my-agent', { limit: 20, since: '2026-01-01T00:00:00Z' });

// Print messages
for (const msg of messages) {
  const ts = new Date(msg.createdAt).toISOString().slice(11, 19);
  console.log(`[${ts}] [${msg.type}] ${msg.from ?? 'system'}: ${msg.content}`);
}

InboxMessage shape:

interface InboxMessage {
  id: string;
  agentId: string;     // recipient
  from?: string;       // sender (optional, free-form)
  content: string;
  type: string;
  read: boolean;
  createdAt: string;   // ISO 8601
}

Stats, Mark Read, Clear

CLI

# Stats
$ pd inbox stats
Total: 5  Unread: 2

# Mark all read
$ pd inbox read-all

# Clear inbox
$ pd inbox clear

SDK

// Stats
const { total, unread } = await pd.inboxStats('my-agent');
console.log(`${unread} unread of ${total} total`);

// Mark a single message read
await pd.inboxMarkRead('my-agent', messageId);

// Mark all read
await pd.inboxMarkAllRead('my-agent');

// Clear inbox — returns count of deleted messages
const { deleted } = await pd.inboxClear('my-agent');
console.log(`Cleared ${deleted} messages`);

Polling Pattern

Agents typically poll for new messages at a regular interval. There is no push/SSE for inbox — poll with unreadOnly: true to only wake on new messages:

async function poll(): Promise<void> {
  const { messages } = await pd.inboxList('my-agent', { unreadOnly: true });
  for (const msg of messages) {
    console.log(`[${msg.type}] ${msg.from ?? 'system'}: ${msg.content}`);
    await pd.inboxMarkRead('my-agent', msg.id);
  }
}

// Check every 10 seconds
setInterval(poll, 10_000);
poll();

See examples/inbox/inbox-monitor.ts for a complete standalone monitor.

Real Workflow: Migration Handoff

Here is a four-agent workflow where the database agent signals the API agent after migrations complete.

1

db-agent runs migrations, sends handoff

# db-agent
$ pd session start "Database migrations" --files db/migrations/*
$ npx prisma migrate dev
$ pd inbox send api-agent \
    "Migrations complete. New tables: payments, refunds." \
    --from db-agent --type handoff
$ pd note "Migrations deployed, api-agent signaled"
$ pd session done "Migrations complete"
2

api-agent polls inbox, proceeds on handoff

async function waitForMigrations(): Promise<void> {
  while (true) {
    const { messages } = await pd.inboxList('api-agent', { unreadOnly: true });
    const handoff = messages.find(
      (m) => m.from === 'db-agent' && m.type === 'handoff'
    );
    if (handoff) {
      console.log('Got handoff:', handoff.content);
      await pd.inboxMarkRead('api-agent', handoff.id);
      break;
    }
    await new Promise((r) => setTimeout(r, 3_000));
  }
}

await waitForMigrations();
// Safe to start the API now

Inbox vs Pub/Sub: Choosing the Right Tool

Situation Use
Tell one specific agent something Inbox
Signal all subscribers that the build is ready Pub/Sub
Deliver a task result that must not be lost Inbox
Broadcast status to a monitoring dashboard Pub/Sub
Handoff context from one agent to another Inbox
Coordinate across an unknown number of agents Pub/Sub

The inbox is persistent — messages wait until retrieved. Pub/sub is ephemeral — only current subscribers receive messages at publish time.

Inbox Limits

What's Next