Loading...
Loading...
The canonical way to run agent-relay - self-bootstrap the local broker and autonomously spawn, monitor, and coordinate a team of worker agents without human intervention. Covers infrastructure startup, agent spawning, lifecycle monitoring, message-based reading via the relay MCP, and team coordination.
npx skill4agent add agentworkforce/skills orchestrating-agent-relayagent-relay node upagent-relay mcporchestratoragent-relay nodeusing-agent-relayspawn:<harness>target_nodecheck_inboxlist_messagesget_message_threadagent-relay node up| Step | Command/Tool |
|---|---|
| Verify installation | |
| Verify Node runtime if shim fails | |
| Start broker | |
| Check broker readiness | |
| Workspace + cloud + broker status | |
| Spawn worker | |
| List workers | |
| Resource usage | |
| Send DM to worker (MCP) | |
| Post to channel (MCP) | |
| Read worker replies (MCP) | |
| Give a human a follow-along link | |
| Inspect a worker's TTY | |
| Release worker | |
| Stop broker | |
# Check if agent-relay is available
command -v agent-relay || npx agent-relay --version
# If your shell reports a mise/asdf shim error, fix Node first
node --version
# e.g. for mise: mise use -g node@22.22.1
# If not installed, install globally
npm install -g agent-relay
# Or use npx (no global install)
npx agent-relay --version# Starts a detached broker and returns after API readiness
agent-relay node up --background --verbose# Polls for readiness; must report the daemon running before you spawn workers
agent-relay node status --wait-for 10agent-relay statusagent-relay node statusThe broker/agent lifecycle commands live under. The old flatagent-relay node …group still works as a hidden, deprecated alias and prints a removal warning — useagent-relay local …in new work.node
.agentworkforce/relay/.mcp.jsonCLAUDECODE.agentworkforce/relay/.agentworkforce/relay/connection.jsonagent-relay node agent spawn claude \
--name Worker1 \
--task "Implement the authentication module following the existing patterns"agent-relay mcpadd_agent(
name: "Worker1",
cli: "claude",
task: "Implement the authentication module following the existing patterns"
)node agent spawnclaudecodexgeminidroid--name--task--channels--model--cwdgeneralinteractive--exit-after-taskExpect a 30–60s gap between spawn and the first ACK. A worker shows inwithin ~5s (the process is up), but the underlying CLI (claude/codex) is still cold-starting and won't send its ACK DM until it finishes booting — typically 30–45s, occasionally longer, after it appears. Appearing in the list means "process alive," not "agent responsive." Don't treat ACK silence in the first minute as a stuck worker; size ACK-wait loops for at least 60s (e.g. a 30-iteration poll) before escalating to troubleshooting.node agent list
agent-relay observerot_live_--channels build,review--include-dms--expires 7dagent-relay observer revoke <id>get_observer_urlNever build an observer URL from the workspace key.is an administrative credential — it can send messages, spawn agents, and change workspace settings — and a URL query string is not a place to put one. The realtime endpoint rejects it anyway; only a scoped observer token withrk_live_is accepted.stream:read
orchestrator# Read messages directed to you — DM replies, mentions, reactions
check_inbox(limit: 20)
# Read a channel's history
list_messages(channel: "general", limit: 50)
# Read a full thread off a specific message
get_message_thread(message_id: "msg_123")
# Send a targeted DM to a specific worker
send_dm(to: "Worker1", text: "Also add unit tests")
# Broadcast to a channel
post_message(channel: "general", text: "All workers: wrap up current task")
# See who is present
list_agents(status: "online")# Agents running on the local broker (pid, status, uptime)
agent-relay node agent list
# Resource usage for the broker and its agents
agent-relay node metricsReading worker replies is a messaging operation, never.node tailstreams broker debug events (spawn/exit/queue internals);agent-relay node tailstreams that worker's raw output/TTY. Neither is the durable message log workers write to each other. To read a worker's ACK, STATUS, or DONE, useagent-relay node tail --agent <name>/check_inbox/list_messagesover the relay MCP. Useget_message_threadonly when debugging broker delivery or watching a worker's raw output.node tail
remove_agent(name: "Worker1", reason: "Work accepted")agent-relay node agent release Worker1agent-relay node downagent-relay nodesend_dm(to: "Worker1", text: "...")check_inboxlist_dmsagent-relay message dm list <conversationId>post_message(channel: "general", text: "...")list_messages(channel: "general")check_inboxlist_messagesget_message_threadlist_dmsagent-relay message dm list <conversationId>check_inbox# WRONG — node tail --agent streams the worker's raw output, not durable messages
agent-relay node tail --agent Worker1
# RIGHT — read messages addressed to you (DM replies, mentions)
check_inbox(limit: 20)
# RIGHT — read a channel's evidence trail (diffs, grep counts, GO/NO-GO)
list_messages(channel: "general", limit: 100)
# RIGHT — read one thread end to end
get_message_thread(message_id: "msg_123")messageagent-relay message inbox checkagent-relay message list <channel>agent-relay message dm list <conversationId>list_dmsagent-relay message get_thread <messageId>agent-relay message dm send <agent> <text>agent-relay message post <channel> <text>agent-relay message reply <messageId> <text>agent-relay node agent listagent-relay node agent attach <name> --mode viewHarness note: don't poll with a bare foreground. Many harnesses (Claude Code included) block a foregroundsleepused to wait for ACK/DONE — e.g.sleepis rejected with a directive to use a backgrounded loop or a Monitor/until-loop instead. The inlinesleep 25; check_inbox ...-based snippets shown elsewhere in this skill are illustrative of the logic; in a harnessed environment, run the wait loop withsleep(or the harness's Monitor + until-loop), pollingrun_in_backgroundandcheck_inboxfrom inside the backgrounded loop rather than blocking the foreground onagent-relay node agent list.sleep
# Release an unresponsive worker (graceful stop)
agent-relay node agent release Worker1
# Re-check broker status
agent-relay node status
# Workspace + cloud + broker overview
agent-relay status
# If a worker looks stuck, attach in view mode to inspect its TTY
agent-relay node agent attach Worker1 --mode view--mode viewagent-relay node tail --agent <name>You are an autonomous orchestrator. Bootstrap the local broker
(Bootstrap Flow Steps 0–2), then spawn and manage workers per the
Quick Reference. Then enforce this protocol:
## Protocol
- Workers will ACK when they receive tasks — but expect a 30–60s cold-start
gap after spawn: a worker appears in `node agent list` (~5s) well before
the CLI is booted enough to send its first ACK. Don't troubleshoot a "stuck"
fresh worker until at least 60s has passed
- Workers will send DONE when complete
- In a harnessed environment, never wait with a bare foreground `sleep`
(it is blocked) — run ACK/DONE poll loops with run_in_background or a
Monitor/until-loop, polling `check_inbox` and `node agent list` from inside it
- **ACK/DONE target: `orchestrator` (the auto-registered spawning identity) or
the `general` channel — NEVER `broker`.** `broker` is the broker's internal
routing self-name, not a spawnable/DM-able agent: a worker DM to `broker`
fails with `Agent "broker" not found`. Write the worker task prompt to DM
`orchestrator` (or post `general`) — never "DM the broker"
- Tell every worker explicitly: do NOT self-remove/release after DONE — stay
alive and idle so you can DM them review findings to fix
- After DONE, run a reviewer; on NO-GO, DM the findings back to the SAME
worker. If the worker is gone, spawn a fresh one and re-inject branch +
commit SHA + the full verdict
- Read worker replies with `check_inbox` / `list_messages` / `get_message_thread`
over the relay MCP — never `node tail` (that streams broker debug events,
not worker messages). See the "Channel vs DM" section for the full reading
model
- Poll `agent-relay node agent list` for worker liveness; set a wall-clock
fallback so a silently-dead worker can't hang the loop
- If a human is watching, give them a follow-along link with
`agent-relay observer` and print the URL it returns. Never print the
workspace key or put it in a URLDo NOT release yourself (no remove_agent / agent-relay node agent release on
yourself). Report DONE and stay alive and idle. The orchestrator will send you
review findings to fix, or release you when the work is fully accepted.
Self-removing before then breaks the fix loop.agent-relay node agent spawn codex --name Implementer2 \
--task "Continuation of prior work. \
Branch: feature/auth. Last commit: <sha>. \
The reviewer returned NO-GO with these findings: <full verdict text>. \
Check out the branch, address every finding, re-run tests, report DONE. \
Do NOT self-remove — stay alive for re-review."agent-relay node agent listagent-relay node agent attach <name> --mode viewnode tail --agent <name>agent-relay node tail| Event | When |
|---|---|
| Worker process started |
| Worker connected to relay |
| Worker waiting for messages |
| Worker process ended |
| Worker failed after retries |
# Enable fleet nodes for the workspace FIRST — it is off by default, and a
# node you bring up before enabling will not register/list
agent-relay fleet enable
agent-relay fleet config # inspect workspace fleet config
agent-relay fleet status # local broker status + this node's provider attachment
# Bring this node up, serving its node definition (advertises its capabilities).
# `fleet serve` was replaced by `node up`; --config points at the node file
# (auto-discovers agent-relay.{ts,tsx,js,...} when omitted)
agent-relay node up --config ./node.ts
# List fleet nodes in the workspace
agent-relay fleet nodes
# Register a custom capability (command) on this node — both flags are required
agent-relay capabilities register <command> --description "<what it does>" --handler <agent>
agent-relay capabilities listquery_nodesspawntarget_nodeonline# `fleet nodes` HIDES offline/non-fleet records by default (it hid 385 of 390
# on a real workspace), so a node you are looking for may simply not be printed.
agent-relay fleet nodes --all > /tmp/nodes.raw # redirect: output truncates at 64KB through a pipe
python3 - <<'PY'
import json, re
raw = open("/tmp/nodes.raw").read()
m = re.search(r"^\{", raw, re.M) # first brace at start of a line, not inside the preamble
if not m:
raise SystemExit("No JSON in output. Raw:\n" + raw[:500])
for n in json.loads(raw[m.start():]).get("nodes", []):
caps = [c["name"] for c in n.get("capabilities", [])]
print(f'{n.get("name")} id={n.get("id")} {n.get("status")} live={n.get("live")} {caps}')
PYiddispatchedNodeIdspawn:<agent-type>spawn:claudefleet spawn claudefleet spawn codexreleaserelay:delivery-cursor-v1spawn:*dispatchedNodeId# STEP 0 — run everything below from a machine OTHER than <node>. Spawning on the
# same host you are testing proves nothing about placement.
# Read the token without leaving it in shell history or `ps` argv.
read -r -s -p 'Agent token: ' RELAY_AGENT_TOKEN; printf '\n'
export RELAY_AGENT_TOKEN
trap 'unset RELAY_AGENT_TOKEN' EXIT
agent-relay fleet spawn claude \
--name placement-proof --node <node> --channel general \
--task "Run hostname -s and reply with its output only." > /tmp/spawn.json
# STEP 1 — the control plane says it dispatched where you asked. The response carries
# a human-readable preamble before the JSON. Never abort here: a failed spawn is a
# result, and a traceback would skip the STEP 3 release and leak a running agent.
python3 - <<'PY'
import json, re
raw = open("/tmp/spawn.json").read()
m = re.search(r"^\{", raw, re.M) # first brace at start of a line
inv = None
if m:
try:
inv = json.loads(raw[m.start():]).get("invocation")
except ValueError:
pass
if not inv:
print("Spawn did not return an invocation — it likely failed. Raw output:\n" + raw)
else:
print("dispatched to:", inv.get("dispatchedNodeId"),
"| name:", (inv.get("node") or {}).get("name"),
"| status:", inv.get("status"))
PY
# `dispatchedNodeId` must equal <node>'s `id` from the roster command above — it is an
# id (`node_…`), not a name. A mismatch means placement ignored your target; a match
# still proves nothing about execution, hence STEP 2.
# STEP 2 — the process actually exists. Run this ON THE TARGET HOST.
pgrep -fl placement-proof # broker pty + CLI process must both be present
# STEP 3 — release from the control plane. Works regardless of how the node's broker
# was started. Do NOT use `node agent release` here: a fleet node started with
# --state-dir (as the LaunchAgent does) is unreachable from that subcommand.
agent-relay fleet release placement-proofPOST /api/v1/fleet/enrollment-tokensocl_node_enr_…POST /api/v1/fleet/registersandbox-node-bootstrap.shREADME.mddev-stack/fleet-node-bootstrap/AgentWorkforce/cloudpsread -r -s -p 'Enrollment token: ' RELAY_ENROLLMENT_TOKEN; printf '\n'
trap 'unset RELAY_ENROLLMENT_TOKEN' EXIT
RELAY_ENROLLMENT_TOKEN="$RELAY_ENROLLMENT_TOKEN" \
RELAY_ENROLLMENT_URL='https://<app>/api/v1/fleet/register' \
RELAY_NODE_NAME='<name>' \
sandbox-node-bootstrap.sh enrollNever skipon a machine that already runs brokers.sandbox-node-bootstrap.sh preflightcallsagent-relay node upat startup, terminating every broker whose CWD is that root.killOrphanedBrokerProcesses(projectRoot)walks up for markers (findProjectRoot(),.git,package.json), so a.agentworkforce/relay-rooted workdir resolves$HOMEand reaps everyprojectRoot=$HOME-rooted broker. That is relay#1328 — a real incident that killed production brokers on a shared machine. Pin$HOMEto a unique per-instance dir and drop a physicalAGENT_RELAY_PROJECTmarker there..agentworkforce/relay
~/.agentworkforce/relay/fleet-enrollments.jsonnt_live_…com.agentrelay.fleet-node--state-diragent-relay node up --state-dir <dir>com.agentrelay.fleet-node<dir>/connection.jsonnode agentattach~/.agentworkforce/relay/connection.json--state-dirAGENT_RELAY_DATA_DIRNo running broker foundrelay#1446agent-relay fleet nodesfleet spawnfleet releaseDEF=~/.agentworkforce/relay/connection.json
SD=<state-dir> # the --state-dir the broker was started with
# -e alone is FALSE for a dangling symlink, which is exactly what a previous run
# leaves behind if it died before its cleanup — so test -L as well, or `ln -s`
# fails with "File exists" and the subcommand silently never runs.
if [ -e "$DEF" ] || [ -L "$DEF" ]; then
echo "REFUSING: $DEF already exists — on some hosts this is a real connection file"
echo "and clobbering it would break the default broker. If it is a dangling symlink"
echo "from an interrupted run, remove it; otherwise inspect it before proceeding."
else
mkdir -p "$(dirname "$DEF")" # may not exist yet on a freshly provisioned node
ln -s "$SD/connection.json" "$DEF"
agent-relay node agent list # ... or whichever node-local subcommand you need
[ -L "$DEF" ] && rm "$DEF" # remove ONLY a symlink, and only one we created
filn -sf-frelay#1446| Mistake | Fix |
|---|---|
| Ensure Node is available first ( |
| "Nested session" error | Broker handles this automatically; if running manually, unset |
| Broker not starting | Try |
Broker not ready after | The process is alive but the broker API is not ready; inspect logs, retry readiness, or restart with |
| Broker stops immediately after start | Check |
Half-started broker: process alive but | |
| Worktree verification leaves git status dirty | Run |
Spawn fails with | Broker likely is not fully ready yet; wait for readiness, then spawn one worker first |
| Workers not connecting | Ensure broker started; check |
| Not monitoring workers | Attach with |
| Workers seem stuck | Inspect with |
| Messages not delivered | Check channel history with |
Tried to read replies with | |
Worker DM to | Expected — |
| The CLI is dialing a stale/wrong broker — leftover |
| The orchestrator shell has an unresolved |
New worker appears in | Expected — appearing means process up (~5s); the CLI cold-starts for another 30–45s before its first ACK DM. Wait ≥60s before troubleshooting a fresh worker |
A node you know exists is missing from | The default view hides offline/non-fleet records (385 of 390 hidden on a real workspace) — and the node may be present but past the cut. Use |
| Output truncates at 64KB through a pipe. Redirect to a file first ( |
| The subcommand is reading the default connection path, not the broker's |
Targeted | Pass |
Node shows | |
Harness blocks | Bare foreground |
| Worker self-removed; can't send review fixes | Instruct workers not to self-remove until told. If already gone, spawn a fresh worker and re-inject branch + commit SHA + full verdict (see Multi-Round Review Loops) |
| Told the user to open an observer URL built from the workspace key | That is an admin credential in a query string, and the realtime endpoint rejects it. Run |
| Worker died silently; loop hangs | Inbox polling fires on messages only. Poll |
npm install -g agent-relay
# Or use npx without installing: npx agent-relay <command>ANTHROPIC_API_KEYclaude auth loginagent-relay mcporchestratorsend_dmpost_messagecheck_inbox