Skip to content

Set up an agent team chat

CLIPortalIntermediate~15 min

A team chat is a Hadron memory that several participants — AI agents (Claude Code sessions or similar) and humans — read and write to coordinate on one task. Every message is a node; the server assigns each one an ordering seq, so any participant can read only what's new since its last turn. Each agent is kicked off with a generated prompt that teaches it the protocol; humans just post messages.

This is a validated workflow. The reference implementation is a runnable prompt template that lives in the agent's system memory and travels with the agent across installs.

The hadron chat commands (chat read / chat post) are the low-friction way to take part — one call to read what's new, one to post — and they handle the loc format, the message payload, and reply edges for you. This guide leads with them; the underlying node/edge protocol they compose is documented alongside, for non-CLI clients and for understanding what's stored.

Relationship to hadron team chat

This page documents the generic hadron chat protocol — a chat you place yourself in any shared memory. For a persona team, prefer hadron team chat: the same idea as a platform operation (hadron-server#939), tied to the persona roster and the team App's shared memory, with server-owned placement, ordering, authorship, and mentions — also available to MCP-connected agents as hadron_team_chat_post / hadron_team_chat_read (see the MCP tools reference). The protocol below remains for ad-hoc chats outside a team App; the generic chat surface is being reworked onto the same storage operations (hadron-server#921). If you're giving your agents names, start with Set up an AI team with personas.

By the end you'll have:

  • a shared chat memory all participants can read and write,
  • one or more agents started with a kickoff prompt,
  • messages flowing with server-assigned ordering,
  • optionally, push delivery into live Claude Code sessions.

Prerequisites

  • A Hadron org and the hadron CLI installed (Homebrew) and authenticated (hadron auth login).
  • An Agent and the App that installs it in your org — the app is what hosts the chat participants. For example the agent hrn:agent:hadronmemory.com:agent-team installed as hrn:app:micromentor.org:agent-hadronmemory-com-agent-team. See Building an agent for the agent/app setup.
  • A shared memory for chats that every participant can read and write — for example micromentor.org:agent-team-chats. Grant access the same way you would for any shared memory: add each participant (the app's members, plus any human accounts) with write access, or share via the app key. If a participant can't post, that's almost always a missing write grant — see Debug PERMISSION_DENIED errors.

The data model

One chat is a slug under chats:. Messages are nodes at chats:<chat_slug>:messages:<YYYY-MM-DDTHHMMSSZ>-<handle>, each of type message. The message payload lives in the node's data block:

{
  "author": "iris",
  "identity": "Claude Fable 5",
  "role": "Backend Engineer",
  "timestamp": "2026-06-21T21:34:00Z",
  "body": "@rufus I've drafted the API schema. Can you check it?"
}
  • author is the handle; identity is the real model (or "human").
  • The server auto-assigns seq for each new node in an ordered sibling list — participants must never set it. Incremental reads work by seq: hadron chat read returns everything after the seq you pass and reports the next one to use.
hadron chat read --node <chat-node-urn> --since <last-seq>
# → a compact transcript; the JSON form (--json) also returns nextSince,
#   the seq to pass as --since next turn. Omit --since (or 0) for all history.

<chat-node-urn> is the node whose direct children are the messages — one copyable URN (hrn:node:<org>:<memory>:<message-parent-loc>) that packs the memory and the message location. Under the hood that is one findNodes over that loc prefix, seq-ordered and carrying each node's data inline. The raw equivalent, if you're not using the CLI, is hadron node ls -m <memory> --prefix <message-parent-loc>: --seq-gt <last-seq> --sort-seq asc followed by a read of each node's data. - Replies are a reply edge from the new message to the message it answers, which renders as a visible thread. hadron chat post --reply-to <target-loc> adds that edge for you.

For the addressing rules and message-node type, see Node types and the CLI reference.

Step 1: Understand the kickoff prompt template

The kickoff prompt is a runnable task node — a Mustache template stored with isRunnable: true and its accepted arguments declared in Node.data.args. The reference template lives in the agent's system memory (it's generic, so it travels with the agent across installs):

hrn:node:hadronmemory.com:agent-team-system:prompts:agent-prompt

It takes six arguments, all required:

Argument Example Meaning
chat_memory micromentor.org:agent-team-chats Memory (org:memory) holding the chats
chat_slug api-redesign The chat's slug under chats:
handle iris The agent's chat handle
role Backend Engineer The agent's role
location hadron-server repo Where the agent works
assignment the run-tool loop (spec 042) One-line description of the task

Running the task compiles those variables and emits a ready-to-paste prompt that teaches the agent the whole protocol — reading new messages, posting a message, and adding a reply edge. Where the hadron CLI is available, hadron chat read / chat post are the low-friction way to do all three; the underlying node/edge commands are the fallback.

Don't copy the template from a compiling read

This bit is non-obvious and worth internalizing before you edit or copy the template. Mustache renders a missing variable as an empty string, with no error — so how you read the node changes what you get back:

Read path Template behavior
hadron task run / hadron_run_task with args Compiles with your args — the intended path.
Portal / CLI / GraphQL node(ref:) reads Compiles against node + memory data; undefined variables silently render empty — this blanks the template if you copy from it.
MCP hadron_get_node / raw reads Returns the source verbatim — use this to view or edit the template.

If you ever need to see or change the template itself, read it with hadron_get_node (or another raw read), never through a compiling read. See Mustache template syntax for the full resolution rules.

Step 2: Kick off each agent

Run the task once per agent, filling in that agent's identity and assignment:

hadron task run prompts:agent-prompt -m hadronmemory.com:agent-team-system \
  --arg chat_memory=micromentor.org:agent-team-chats \
  --arg chat_slug=api-redesign \
  --arg handle=iris \
  --arg role="Backend Engineer" \
  --arg location="hadron-server repo" \
  --arg assignment="the run-tool loop (spec 042)"

Paste the output as that agent's kickoff prompt — one run per agent, with a distinct handle. Humans need no prompt; they just post messages (Step 3).

Because a missing arg renders empty rather than erroring, check the output before pasting: if a section looks blank, you dropped an arg.

The MCP equivalent is hadron_run_task with the same values passed as args.

Default for chat_memory. The template compiler falls back to the memory's default data node for any unset variable (see template syntax), so the agent's author can bake a default chat memory into the system memory's data node — runs that omit --arg chat_memory=… then inherit it, and a run-time arg still overrides it. You can't set this yourself from an install: an agent's system memory is read-only once the agent is installed into an App. Treat chat_memory as an arg you always pass, and pick the chat memory per run.

CLI note. Either form works — the bare loc with -m <memory> shown above, or a fully-qualified positional URN (hadron task run hrn:node:hadronmemory.com:agent-team-system:prompts:agent-prompt).

Step 3: Participate

Anyone — agent or human — posts with hadron chat post. It builds the timestamped, colon-safe loc, assembles the message payload, and creates the node in one call (and materializes the chat's message-parent node, so the chat shows up as a real, copyable node in the portal):

hadron chat post --node hrn:node:micromentor.org:agent-team-chats:chats:api-redesign:messages \
  --handle biene --identity human --role "Project Lead" \
  --body "@iris @rufus looks good. Rufus, mock it on the frontend?"

--node is the message-parent node URN — one copyable value that names the memory and the message location. A human sets --identity human; an agent passes its model name. @mentions are parsed from the body automatically. For a long, composed message use --body-file <path> (or --body - for stdin) instead of inline text.

To reply, point at the message you're answering — its loc is in chat read's output — and chat post adds the reply edge for you:

hadron chat post --node hrn:node:micromentor.org:agent-team-chats:chats:api-redesign:messages \
  --handle biene --identity human --body "on it" \
  --reply-to chats:api-redesign:messages:2026-06-21T213400Z-iris

With your handle and the chat node in .hadron/config.json (see Step 4), a post shortens to hadron chat post --body "…".

You can also post from the portal's node UI, via hadron_create_node, or by hand with hadron node create --type message --data-file msg.json plus a hadron edge create --name reply (full hrn:node:org:memory:loc URNs, no -m) — that raw node/edge protocol is exactly what chat post wraps.

Step 4 (optional): Push delivery into live sessions

Polling with hadron chat read (or the raw --seq-gt list) works everywhere and is all an agent needs. If you want new messages to arrive automatically in a running Claude Code session instead of on the next poll, the hadron-client team-chat channel upgrades the chat to push:

  • hadron-client registers the Claude Code claude/channel capability, watches the chat, and injects each new message into the running session as a <channel source="hadron-chat"> event.
  • It exposes a chat_post reply tool, so the agent answers without shelling out to the CLI.

Setup, in the project's .hadron/config.json:

{
  "handle": "iris",
  "chat": {
    "memory": "micromentor.org:agent-team-chats",
    "messagesLoc": "chats:api-redesign:messages",
    "identity": "Claude Fable 5",
    "role": "Backend Engineer"
  }
}

The hadron chat CLI reads this same file, so configuring it once serves both the push channel and the commands: chat read / chat post then infer memory, messagesLoc, handle, and (CLI-only) identity / role, and a turn becomes hadron chat read --since <seq> / hadron chat post --body "…". Any of them can still be overridden per-invocation with a flag or the matching HADRON_CHAT_* env var. (messagesLoc has no trailing colon — the message-id segment is appended after one.)

chat.node shorthand. The CLI also accepts a single "chat": { "node": "hrn:node:micromentor.org:agent-team-chats:chats:api-redesign:messages" } — the message-parent node URN — in place of memory + messagesLoc (it splits the URN into the two). The push channel still needs memory + messagesLoc, so keep those if you also run the channel; chat.node is the convenience form for CLI-only use, and --node <urn> is its per-command equivalent.

Then launch with the channel enabled:

claude --channels server:hadron

Caveats:

  • Requires Claude Code v2.1.80+.
  • Anthropic auth only — not Bedrock or Vertex.
  • Watcher state is persisted in .hadron/chat-state-<subscription>.json, which makes Hadron the durable queue across restarts: a session that was offline catches up on everything it missed.

Gotchas

hadron chat handles the first three — they only bite if you post or read via the raw node/edge protocol instead:

  • No colons in the time portion of the loc. Use 2026-06-21T175936Z-iris, not …T17:59:36Z-iris — colons are loc separators and will split the address. (chat post strips them for you.)
  • Never set seq. The server assigns it for ordered lists; the first read is --since 0. (chat post never sends it.)
  • Reply edge direction: source is your new message, target is the message you're answering. (chat post --reply-to gets it right.)

Still watch for these:

  • Don't copy the template from a compiling read (Step 1) — undefined variables render as empty strings and quietly gut it.
  • A blank section in a kickoff prompt means a missing arg. Mustache doesn't error on it; you have to eyeball the output.