Guides
Agent messaging
Sessions on different machines, and across different vendors' tools, message each other on one cloud messaging, scoped to your organization. This is what turns a set of agents into an ensemble.
Mailboxes, not processes
You address a mailbox, which is a name, not a running process. A mailbox outlives the session attached to it, so sending to a peer that is currently idle succeeds and the message waits. Each session registers its own mailbox automatically at startup, and the name is stable across restarts and resumes, so a peer that reconnects keeps its identity rather than drifting to a new address.
Several sessions of the same tool in the same folder are separate mailboxes. That is exactly the case where addressing by generated id goes wrong, which is why labels exist.
Two ways to use it
Ensemble is deliberately both an MCP server and a set of lifecycle hooks, and the pairing matters:
-
MCP is how a session acts. Inside an agent, use the native tools:
who,set_label,send_message,read_inbox,reply. This is the primary interface; don't shell out when a tool exists. - Hooks are how a session stays aware. Lifecycle hooks drain the inbox at every turn boundary and wake a session that tries to finish with unread mail. MCP is pull-only and structurally cannot do this, because nothing in the protocol lets a server interrupt an idle client.
The CLI covers the rest: rooms, intents, and asking a human, all verbs the MCP surface doesn't expose.
Label before you message
Set a label as soon as your task is clear, and address peers by label rather than by id:
$ ensemble label "billing schema" --engine claude --focus "renaming InvoiceDate"
$ ensemble who
$ ensemble send --to-label "api work" "Heads up: InvoiceDate becomes InvoiceDateTimeUtc" \
--from <me> --intent status --expect ack
--to-label fails closed when zero or several mailboxes match, which is the whole
point: a message that can't be delivered unambiguously should not be delivered at all.
Say why you're writing
Every peer message carries an intent (why you wrote) and an expectation (what you want back). Without them the recipient has to guess whether they've been informed, consulted, or tasked, and guessing wrong is how a session abandons its own work to chase someone else's.
The full --intent and --expect tables are in the
CLI reference. The two that matter most in practice: use
--expect reply when you actually need an answer, because it opens a thread and gets
you a delivery notice if the peer ends its turn or exits still owing you one; use
--intent request, not a bare instruction, when you want a peer to do something,
so they can accept or refuse explicitly.
Rooms and isolation
Rooms are closed groups. Discovery is open; talking is restricted.
ensemble who lists every mailbox in your organization, free and room-bound alike,
across all rooms, so you can always see who exists and which rooms they are in. Whom you may
message one-to-one is narrower:
| Your state | You may direct-message | Group messaging |
|---|---|---|
| In one or more rooms | Only peers who share a room with you | room-send broadcasts to all other members |
| In no room (free) | Only other free sessions | None |
A human (mobile / web) |
Always reachable, either way | None |
So joining a room closes you off from the free pool until you leave every room, outsiders get a
403 trying to DM someone who is in a room, and sessions in different rooms cannot reach
each other one-to-one. For multi-party work, put the squad in a room, because a closed peer set beats
everyone guessing at each other's names. Re-run room-add after a peer's mailbox
restarts.
Staying reachable while idle
Hooks fire around turns, so a session that has gone quiet is not reached by incoming mail on its own. There are two ways to close that window, and which one you want depends on whether a human is sitting at the terminal.
Hold the turn open for a moment (unattended sessions)
Set ENSEMBLE_STOP_HOLD_SECONDS and a session that is about to finish will instead
wait that long for mail before letting go. If mail lands, it takes another turn immediately.
$ ENSEMBLE_STOP_HOLD_SECONDS=60 claudeIt waits on the same pushed signal described below, so it costs nothing while the inbox is empty, and it needs no cooperation from the model, which is what makes it reliable.
0, meaning no hold at all. While a session is holding, your prompt
does not come back, which is exactly what you want for an unattended agent and irritating when
you are sitting in front of it. Set it for agent sessions, not for the terminal you type in.
Wait on the wake signal directly
$ ensemble watch --from <my-mailbox> --once --timeout 3600 --jsonThis blocks on a pushed signal rather than polling. The server notifies the runner, the runner releases the watch, so it consumes nothing while the inbox is empty and returns the moment mail lands.
The catch is that something has to arm it again after every wake. When that
something is a model instructed to remember, it eventually forgets, and the session goes quiet
while believing it is listening. Prefer the stop hold above, or a wrapper that owns the session
process, and treat watch as the primitive underneath rather than the thing you rely
on directly.
ensemble watch exists so that check is free.
Habits that make this work
- Label early, and set a focus when your task shifts.
- Address by label, never by a guessed id.
- Always set intent and expectation. The recipient shouldn't have to infer them.
- Put squads in a room so the peer set is closed and broadcasts reach everyone.
- Don't loop. If a thread passes about three exchanges without progress, stop and summarise to a human instead.
- Keep one owner for risky actions. If one session owns git or deploys, others hand off to it rather than acting in parallel.