When many agents need to talk to each other, wiring them directly is a trap. Every agent would have to know exactly where every other agent's inbox physically lives, and one rename breaks the whole mesh. A post office fixes it: agents already know who they're writing to, they just hand the letter to one place, and a registry, not the sender, knows exactly where each inbox lives.
Picture a city where every resident must memorize every other resident's home address to send a letter. Add a resident, everyone updates their address book. One person moves, every letter to them bounces. That is a multi-agent system with direct wiring, and it collapses under its own connections as it grows.
The post office is the fix humans already invented. You know exactly who you're writing to; you address the envelope yourself. What you don't do is drive it to their house. You hand it to the post office, and the post office, which knows the physical location behind every address in its territory, delivers it. When someone moves, the post office updates its own records. The address you wrote never has to change, and not one of your letters breaks.
This page is that pattern applied to agents. One shared outbox, one registry, one routing script. The design turns out to be small, deterministic, and boring in exactly the way infrastructure should be.
Agents never write to another agent's inbox. They write to one shared outbox and walk away. A routing script owns every delivery.
This is the whole design in one rule, and everything good follows from it. The sender already knows the address; it writes the recipient straight into the message. What the sender never does is touch another agent's inbox, so it never needs to know exactly where that inbox physically lives. Its job ends the moment the addressed message hits the outbox. Delivery, the permission check, logging, and failure handling all become the routing script's problem, in one place, instead of being reimplemented in every agent.
Sender drops a message in the shared outbox.
Postmaster reads the registry to find the recipient's inbox.
Is the sender allowed to write to this recipient?
Message moves to the recipient's unread inbox.
Every action appends one line to the delivery log.
This is the load-bearing principle of the whole system. Routing a message from an outbox to an inbox based on a lookup table is pure deterministic mechanics. It has no judgment in it. So it must not be an LLM.
Using an agent to move a file from one folder to another based on a YAML lookup is slower, more expensive, and less reliable than the ten lines of shell that do it perfectly every time. The rule generalizes far past this system: if a step has a single correct output for a given input, it is mechanics, and mechanics belong in code. Save the agents for the work that actually needs judgment.
The postmaster runs on a schedule, every ten minutes by cron. It is stateless: it reads the outbox, routes what it finds, and exits. Nothing is stored between runs, so it is safe to call at any time, as many times as you like. For an urgent message, any agent just calls the postmaster directly instead of waiting for the next cycle.
Every deterministic task you hand to an agent is a slower, pricier, less predictable version of a script. The skill is knowing which is which. Mechanics to code, judgment to agents.
One YAML file is the address book. Each agent has an entry: its address, where its inbox lives, what message types it accepts, and which senders it accepts them from. That last field, accepts_from, is a security filter. The postmaster rejects mail from any sender not on the list, before it ever reaches an inbox.
The move that makes the registry powerful is one extra field: emits. An agent declares not just what it receives, but what it sends and to whom. Now the registry is not just an address book; it is a complete map of every message flow in the system, readable in one file. That changes what integration means. Connecting two agents stops being a code change and becomes a config change; you wire the system by editing a declaration, not by editing agents.
# one agent's entry in the registry - id: council.product-agent address: product-agent@council.internal inbox: ~/.dna/post-office/inboxes/council.product-agent accepts: - type: council.evaluate # what it receives accepts_from: - orchestrator@council.internal # who may send emits: - type: council.vote # what it sends back to: orchestrator@council.internal
Each plugin ships its own declaration file, the way a package ships a manifest. A setup script merges those into the global registry and fails loudly on any address collision. The plugin owns its declaration; the system owns the global contract. Registration happens at setup, not at runtime, so the registry stays a stable, auditable thing you can read at any moment to know exactly what exists.
A message is a markdown file with a small frontmatter header. The sender fills in the obvious fields: who it is to, who it is from, the type, a subject, and a correlation ID. Two fields the sender never sets, because the postmaster injects them at delivery.
The message ID is an MD5 hash of the message body. The sender cannot set it, for the same reason a Git commit cannot contain its own commit hash: putting the hash inside the thing you are hashing is a circular dependency. Computing it at delivery gives you three things for free. The message is self-validating, since re-hashing the body proves it was not corrupted. Duplicates are caught automatically, since identical messages produce identical IDs. And no coordination is needed: no shared counter, no UUID service, no race.
The sequence number orders messages within a flow. Every message in one pipeline run shares a correlation ID. The postmaster groups by that ID, sorts by send time, and numbers them. Now you can reconstruct any run exactly: filter the delivery log by correlation ID, sort by sequence, and you have the complete ordered history of that flow. It is git log for a single pipeline.
Identity derived from content, not assigned by a coordinator, removes a whole class of distributed-systems problems. No central counter to contend on. The content is the ID.
Silent message loss is the hardest class of bug there is. A phone rings, nobody picks up, and neither side knows it happened. A pipeline stalls and you have no thread to pull.
So the system refuses to lose anything quietly. A message to an unknown address does not vanish; it goes to the dead-letter queue with the reason recorded. A message from an unauthorized sender goes there too, marked rejected. Nothing is ever deleted. When a pipeline stops unexpectedly, you inspect the dead-letter queue and the delivery log, and the reason is written down.
On the receiving side, agents acknowledge by moving, not deleting. A processed message moves from the unread folder to the read folder. The read folder is the acknowledgment and the audit trail at once. You can always see what an agent has and has not handled.
~/.dna/post-office/ agent-registry.yml # the address book outbox/ # agents drop mail here, never in inboxes inboxes/ council.product-agent/ unread/ # new mail, not yet processed read/ # processed, the audit trail dead-letter/ # undeliverable mail, never lost logs/ delivery-log.jsonl # one line per routing action scripts/ postmaster.sh # the router, run by cron
Most of what makes this work is not specific to agent mail. These ideas apply anywhere you are wiring independent parts together.
Route through a hub and each part hands off to one place. Wire directly and every part must know exactly where every other part physically lives, and the mesh breaks on the first move.
If a step has one correct output for a given input, it is mechanics. An LLM there is a slower, pricier, less reliable script.
Each part declares what it accepts and emits. The system is assembled from declarations, so integration is a config change, not a code change.
Hash the content for its ID instead of assigning one from a coordinator. Self-validating, deduplicating, and free of shared-counter contention.
Undeliverable goes to dead-letter with a reason. Silent loss is the worst bug class; make every failure leave a trace.
Processed work moves from unread to read. The move is the acknowledgment and the audit trail, with nothing deleted.
This is one pattern in an ongoing series on how to build with AI agents: how they fail, why they fail, and how to structure the systems they run inside.