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"
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
Send from the terminal
$ pd inbox send agent-bob "Schema migration complete, ready for review"
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"}'
Send from JavaScript
await pd.inboxSend('agent-bob', 'Schema migration complete', {
from: 'agent-alice',
type: 'handoff',
});
Message types are free-form strings. Common conventions:
message— general notehandoff— "I finished X, you can proceed with Y"alert— something needs attentionresult— output of a completed task
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.
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"
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
- 1000 messages per agent inbox. The oldest messages are dropped when the limit is reached.
- Messages have no TTL — they remain until read or explicitly cleared with
inboxClear(). - There is no SSE/push notification for inbox messages. Poll with
inboxList({ unreadOnly: true }).
What's Next
examples/inbox/agent-dm.sh— Shell script walkthrough of the full inbox lifecycleexamples/inbox/inbox-monitor.ts— TypeScript polling monitor you can run right now- Multi-Agent Orchestration tutorial — Sessions, pub/sub, and locks in depth
- SDK Reference — Full method signatures and type definitions