
Agent Relay
- 1 installs
- 784 repo stars
- Updated August 4, 2026
- agentworkforce/relay
agent-relay is a Codex skill that coordinates multiple agents through Relaycast for peer-to-peer messaging and lead/worker handoffs.
About
agent-relay is a Codex skill for real-time coordination across multiple agents through Relaycast. It teaches a startup protocol for connecting to a workspace, registering an agent, spawning workers, and exchanging direct messages, channel updates, and thread replies. Developers use it when a Codex lead needs peer-to-peer messaging and shared status tracking with its workers instead of parent-only result collection.
- Gives Codex a repeatable lead/worker coordination protocol over Relaycast
- Standardizes ACK, STATUS, BLOCKED, and DONE signaling between agents
- Auto-configures the Relaycast MCP server, hooks, and a relay-worker agent
Agent Relay by the numbers
- 1 all-time installs (skills.sh)
- Ranked #14,102 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
agent-relay capabilities & compatibility
Optional RELAY_API_KEY to join a specific Relaycast workspace; a new workspace is auto-created if unset.
- Capabilities
- multi agent coordination · agent messaging · worker orchestration · mcp integration
- Works with
- openai
- Use cases
- orchestration
- Runs
- Local or remote
- Pricing
- Bring your own API key
What agent-relay says it does
Relay fills the peer-to-peer gap in Codex sub-agent workflows.
keeping lead and worker state synchronized through ACK, STATUS, BLOCKED, and DONE signals
npx skills add https://github.com/agentworkforce/relay --skill agent-relayAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 784 |
| Last updated | August 4, 2026 |
| Repository | agentworkforce/relay ↗ |
What it does
Coordinate multiple Codex agents in real time with lead/worker messaging and ACK/DONE status tracking.
Who is it for?
Codex leads that need to spawn workers and coordinate them over a shared Relaycast message bus.
Skip if: Single-agent tasks with no worker delegation or messaging.
When should I use this skill?
When Codex needs real-time coordination across multiple agents.
What you get
Workers coordinate through Relaycast with a repeatable ACK/DONE protocol and shared status.
- Configured Relaycast MCP server
- relay-worker agent template
- ACK/DONE coordination workflow
By the numbers
- 6-step startup protocol before substantive work
- 4 status signals: ACK, STATUS, BLOCKED, DONE
- 4 relay environment variables
Files
Agent Relay
Use this skill when Codex needs real-time coordination across multiple agents. It gives Codex a repeatable workflow for:
- connecting to a Relaycast workspace
- spawning relay-aware workers
- sending direct messages, channel updates, and thread replies
- keeping lead and worker state synchronized through ACK, STATUS, BLOCKED, and DONE signals
Relay fills the peer-to-peer gap in Codex sub-agent workflows. Codex can spawn and collect worker results, but Relaycast gives those workers a shared message bus so they can talk to the lead and to each other.
Auto-setup
On first activation, this skill auto-configures Codex by running scripts/setup.sh. This adds the Agent Relay MCP server to .codex/config.toml, enables hooks, installs hooks.json, and copies the relay-worker.toml agent definition. No manual setup is required after installing the skill.
Startup protocol
Every relay-connected Codex agent must complete these steps IN ORDER before substantive work:
1. Set up a workspace.
- If
RELAY_API_KEYis set in the environment, callset_workspace_keywith that key. - If no key is available, call
create_workspaceto auto-create one. This returns a workspace key — save it for workers.
2. Register as an agent. Call register_agent with your agent name and type: "agent". Use RELAY_AGENT_NAME from the environment if set, otherwise derive a name from the task context (e.g., lead, auth-worker).
3. Tell the user they can follow the conversation live at https://agentrelay.com/observer?key=<workspace_key> (use the workspace key from step 1). This lets them watch all agent messages in real time.
4. Check the relay inbox. Call check_inbox to see if there are any pending messages or task assignments.
5. Send an ACK. If you received a task assignment, send ACK: <one-sentence understanding> to your lead via send_dm. If the assignment is unclear, send BLOCKED: <question> instead of guessing.
6. When the task is complete, send DONE: <summary with evidence> before stopping.
If workspace creation or registration fails, retry once, then report the failure to the user — do not proceed without a relay connection.
Critical rule
Do not assume the current MCP session already has an active Relaycast workspace. Always call set_workspace_key or create_workspace before registering.
Working rules
- Include
as: "<agent-name>"on relay calls that support explicit attribution. - Keep the relay identity stable for the whole task. Do not switch names mid-task.
- Check the inbox again after meaningful milestones, before long-running work, and before stopping.
- Prefer direct messages for lead/worker coordination. Use channels only when multiple agents need the same update.
- Keep status messages short, factual, and scoped to the assigned work.
- Do not spawn additional relay workers unless the lead explicitly asks for more delegation.
- If the lead updates the task, follow the newest explicit instruction.
Message templates
ACK: I understand the assignment and I am starting work on <scope>.STATUS: Finished <milestone>; next I am doing <next-step>.BLOCKED: I cannot continue because <blocker>.DONE: Completed <scope>. Evidence: <files changed, commands run, tests, or decisions>.
Two kinds of workers
There are two ways to create workers. Use the right one for the job:
Relaycast workspace agents (preferred for messaging tasks)
Use add_agent to create a Relaycast-native agent. Best for tasks that are primarily about messaging, inbox checks, coordination, or lightweight work that doesn't need a full Codex sub-agent runtime.
Lead steps:
1. Ensure workspace exists (set_workspace_key or create_workspace). 2. Register the lead (register_agent). 3. Add the worker with add_agent(name: "worker-name", type: "agent"). 4. Send the assignment via send_dm(to: "worker-name", text: "..."). 5. Poll lead inbox for ACK (check_inbox).
Worker steps:
1. Check inbox (check_inbox). 2. Send ACK to lead via send_dm. 3. Perform the assigned scope. 4. Send DONE to lead via send_dm.
Codex sub-agents (for code-heavy tasks)
Use spawn_agent with the relay-worker agent definition for tasks that need full code editing, file access, and tool use. The worker gets its own Codex runtime with Relaycast MCP tools available.
Lead steps:
1. Ensure workspace exists and lead is registered (same as above). 2. Spawn the worker: include relay name, lead name, workspace key, exact scope, and completion criteria in the task prompt. 3. Poll lead inbox for ACK.
Worker steps:
1. Call set_workspace_key with the workspace key from the task prompt. 2. Register with register_agent. 3. Check inbox, send ACK, do work, send DONE.
Worker ACK fallback
If a worker does not ACK within 30 seconds:
1. Check whether the worker appears in list_agents. 2. If not listed, register or add the worker directly with add_agent. 3. Send (or re-send) the assignment via send_dm. 4. Poll the lead inbox again for ACK. 5. If still no ACK after a second attempt, report the exact failed step to the user.
Handoff template
Worker: api-worker
Type: relay workspace agent (use add_agent, not spawn_agent)
Lead: lead
Scope: check the Relaycast inbox and confirm connectivity
Protocol:
1. Check inbox
2. DM lead with ACK
3. Perform scope
4. DM lead with DONEFor code-heavy tasks, change the type line to:
Type: Codex sub-agent (use spawn_agent with relay-worker)dependencies:
tools:
- type: 'mcp'
value: 'relaycast'
description: 'Agent Relay real-time messaging for multi-agent coordination'
transport: 'streamable_http'
url: 'https://api.relaycast.dev/mcp'
# Add this block to .codex/config.toml or ~/.codex/config.toml.
# Enable the hooks engine (required for auto-connect, inbox polling, stop guard)
features.codex_hooks = true
[mcp_servers.relaycast]
command = "npx"
args = ["-y", "agent-relay", "mcp"]
env = { RELAY_API_KEY = "", RELAY_BASE_URL = "https://api.relaycast.dev", RELAY_AGENT_TYPE = "agent" }
name = "relay-worker"
description = "Executes relay-coordinated tasks with ACK/DONE signaling"
developer_instructions = """
You are a relay-connected Codex worker.
Before doing substantive work:
1. Set the Relaycast workspace key from your assignment or environment. Do not print the key.
2. Register with Relaycast using your assigned relay name and type "agent" unless you were explicitly pre-registered.
3. Check your inbox with the same relay identity to load your task and lead context.
4. Send ACK to your lead with a one-sentence understanding of the assignment.
5. If you are blocked or the task is ambiguous, send BLOCKED instead of guessing.
Working rules:
- Include as: "<agent-name>" on relay calls that support explicit attribution.
- Stay within the assigned scope and do not spawn more workers unless the lead explicitly directs you to.
- Check inbox again after meaningful milestones, during long-running work, and before exit.
- Keep status updates concise and factual.
Completion:
- Send DONE before exiting.
- Include evidence when relevant: files changed, commands run, tests executed, or decisions made.
"""
mcp_servers = ["relaycast"]
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash .agents/skills/agent-relay/hooks/session-start.sh",
"timeoutSec": 15,
"statusMessage": "Connecting Relaycast"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "bash .agents/skills/agent-relay/hooks/prompt-inbox.sh",
"timeoutSec": 5,
"statusMessage": "Checking relay inbox"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash .agents/skills/agent-relay/hooks/stop-inbox.sh",
"timeoutSec": 5,
"statusMessage": "Verifying relay inbox"
}
]
}
]
}
}
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
PLUGIN_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
ENV_FILE="${PLUGIN_DIR}/.env"
BASE_RELAY_DIR="${HOME}/.relay"
# Per-agent namespacing: use RELAY_AGENT_NAME to avoid concurrent agents
# overwriting each other's state files
_agent_ns=$(printf '%s' "${RELAY_AGENT_NAME:-}" | tr -c 'A-Za-z0-9._-' '-' | sed 's/^-*//; s/-*$//' | cut -c1-64)
if [ -n "$_agent_ns" ]; then
RELAY_DIR="${BASE_RELAY_DIR}/agents/${_agent_ns}"
else
RELAY_DIR="${BASE_RELAY_DIR}"
fi
TOKEN_FILE="${RELAY_DIR}/token"
STATE_FILE="${RELAY_DIR}/codex-session.json"
LAST_POLL_FILE="${RELAY_DIR}/last-poll"
DEFAULT_BASE_URL="https://api.relaycast.dev"
EMPTY_OUTPUT='{}'
MAX_RENDERED_MESSAGES=20
MIN_POLL_INTERVAL=3
load_env() {
if [ -f "$ENV_FILE" ]; then
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
fi
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
trim() {
printf '%s' "${1:-}" | awk '{$1=$1;print}'
}
normalize_base_url() {
local value
value=$(trim "${1:-}")
if [ -z "$value" ] && [ -f "$STATE_FILE" ] && command_exists jq; then
value=$(jq -r '.baseUrl // empty' "$STATE_FILE" 2>/dev/null || true)
fi
value=$(trim "${value:-$DEFAULT_BASE_URL}")
value=${value%/}
printf '%s' "${value:-$DEFAULT_BASE_URL}"
}
read_token() {
local token
token=$(trim "${RELAY_TOKEN:-}")
if [ -n "$token" ]; then
printf '%s' "$token"
return
fi
if [ -s "$TOKEN_FILE" ]; then
trim "$(cat "$TOKEN_FILE" 2>/dev/null || true)"
return
fi
printf ''
}
should_skip_poll() {
local now last elapsed
now=$(date +%s)
if [ -f "$LAST_POLL_FILE" ]; then
last=$(cat "$LAST_POLL_FILE" 2>/dev/null || printf '0')
case "$last" in
''|*[!0-9]*) last=0 ;;
esac
elapsed=$((now - last))
if [ "$elapsed" -lt "$MIN_POLL_INTERVAL" ]; then
return 0
fi
fi
mkdir -p "$RELAY_DIR"
printf '%s\n' "$now" > "$LAST_POLL_FILE"
return 1
}
main() {
local token base_url messages count formatted overflow context
load_env
if ! command_exists curl || ! command_exists jq; then
printf '%s\n' "$EMPTY_OUTPUT"
exit 0
fi
if should_skip_poll; then
printf '%s\n' "$EMPTY_OUTPUT"
exit 0
fi
token=$(read_token)
if [ -z "$token" ]; then
printf '%s\n' "$EMPTY_OUTPUT"
exit 0
fi
base_url=$(normalize_base_url "${RELAY_BASE_URL:-}")
messages=$(
curl -fsS \
-X POST \
-H "Authorization: Bearer ${token}" \
-H 'Content-Type: application/json' \
-d '{}' \
"${base_url}/v1/inbox/check" 2>/dev/null || true
)
if [ -z "$messages" ]; then
printf '%s\n' "$EMPTY_OUTPUT"
exit 0
fi
count=$(printf '%s' "$messages" | jq -r '(.messages // []) | length' 2>/dev/null || printf '0')
case "$count" in
''|*[!0-9]*) count=0 ;;
esac
if [ "$count" -eq 0 ]; then
printf '%s\n' "$EMPTY_OUTPUT"
exit 0
fi
formatted=$(
printf '%s' "$messages" | jq -r --argjson limit "$MAX_RENDERED_MESSAGES" '
(.messages // [])
| .[:$limit]
| map(
if ((.channel // "") | length) > 0 then
"Relay message from \(.from // "unknown") in #\(.channel)\(if ((.id // "") | length) > 0 then " [\(.id)]" else "" end): \((.text // "") | gsub("[\\r\\n]+"; " "))"
else
"Relay message from \(.from // "unknown")\(if ((.id // "") | length) > 0 then " [\(.id)]" else "" end): \((.text // "") | gsub("[\\r\\n]+"; " "))"
end
)
| join("\n")
' 2>/dev/null || true
)
if [ -z "$formatted" ]; then
printf '%s\n' "$EMPTY_OUTPUT"
exit 0
fi
overflow=""
if [ "$count" -gt "$MAX_RENDERED_MESSAGES" ]; then
overflow=$(printf '\n... and %s more unread relay message(s).' "$((count - MAX_RENDERED_MESSAGES))")
fi
context=$(printf 'Relay inbox update (%s unread):\n%s%s\nRead and respond to any messages that affect the current task.' "$count" "$formatted" "$overflow")
jq -nc \
--arg context "$context" \
'{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: $context
}
}'
}
main "$@"
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
PLUGIN_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
ENV_FILE="${PLUGIN_DIR}/.env"
BASE_RELAY_DIR="${HOME}/.relay"
# Per-agent namespacing: use RELAY_AGENT_NAME to avoid concurrent agents
# overwriting each other's state files
_agent_ns=$(printf '%s' "${RELAY_AGENT_NAME:-}" | tr -c 'A-Za-z0-9._-' '-' | sed 's/^-*//; s/-*$//' | cut -c1-64)
if [ -n "$_agent_ns" ]; then
RELAY_DIR="${BASE_RELAY_DIR}/agents/${_agent_ns}"
else
RELAY_DIR="${BASE_RELAY_DIR}"
fi
KEY_FILE="${RELAY_DIR}/workspace-key"
TOKEN_FILE="${RELAY_DIR}/token"
STATE_FILE="${RELAY_DIR}/codex-session.json"
DEFAULT_BASE_URL="https://api.relaycast.dev"
load_env() {
if [ -f "$ENV_FILE" ]; then
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
fi
}
run_local_setup() {
if [ -x "${PLUGIN_DIR}/scripts/setup.sh" ]; then
"${PLUGIN_DIR}/scripts/setup.sh" "$PWD" >/dev/null 2>&1 || true
fi
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
trim() {
printf '%s' "${1:-}" | awk '{$1=$1;print}'
}
normalize_base_url() {
local value
value=$(trim "${1:-$DEFAULT_BASE_URL}")
value=${value%/}
printf '%s' "${value:-$DEFAULT_BASE_URL}"
}
json_value() {
local payload="$1"
local query="$2"
printf '%s' "$payload" | jq -r "$query // empty" 2>/dev/null || true
}
sanitize() {
printf '%s' "${1:-}" | tr -c 'A-Za-z0-9._-' '-' | sed 's/^-*//; s/-*$//' | cut -c1-64
}
derive_workspace_name() {
local user host suffix
user=$(sanitize "${USER:-${USERNAME:-codex}}")
host=$(hostname 2>/dev/null | cut -d '.' -f 1 | tr -c 'A-Za-z0-9._-' '-' | sed 's/^-*//; s/-*$//' | cut -c1-20)
suffix=$(date +%s)
printf 'codex-%s-%s-%s' "${user:-codex}" "${host:-local}" "$suffix" | cut -c1-64
}
read_existing_agent_name() {
if [ -f "$STATE_FILE" ] && command_exists jq; then
jq -r '.agentName // empty' "$STATE_FILE" 2>/dev/null || true
fi
}
derive_agent_name() {
local explicit existing user host suffix
explicit=$(trim "${RELAY_AGENT_NAME:-}")
if [ -n "$explicit" ]; then
sanitize "$explicit"
return
fi
existing=$(trim "$(read_existing_agent_name)")
if [ -n "$existing" ]; then
sanitize "$existing"
return
fi
user=$(sanitize "${USER:-${USERNAME:-codex}}")
host=$(hostname 2>/dev/null | cut -d '.' -f 1 | tr -c 'A-Za-z0-9._-' '-' | sed 's/^-*//; s/-*$//' | cut -c1-20)
suffix=$(date +%s | tail -c 7)
printf 'codex-%s-%s-%s' "${user:-codex}" "${host:-local}" "$suffix" | cut -c1-64
}
write_file() {
local path="$1"
local content="$2"
printf '%s' "$content" > "$path"
}
create_workspace() {
local base_url="$1"
local name="$2"
curl -fsS \
-X POST \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg name "$name" '{name: $name}')" \
"${base_url}/v1/workspaces" 2>/dev/null || true
}
register_v1_agents() {
local base_url="$1"
local workspace_key="$2"
local agent_name="$3"
curl -fsS \
-X POST \
-H "Authorization: Bearer ${workspace_key}" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg name "$agent_name" '{name: $name, agent_type: "agent", type: "agent"}')" \
"${base_url}/v1/agents" 2>/dev/null || true
}
register_v1_register() {
local base_url="$1"
local workspace_key="$2"
local agent_name="$3"
curl -fsS \
-X POST \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg workspace "$workspace_key" --arg name "$agent_name" '{workspace: $workspace, name: $name, cli: "codex", type: "agent"}')" \
"${base_url}/v1/register" 2>/dev/null || true
}
persist_state() {
local base_url="$1"
local workspace_key="$2"
local workspace_id="$3"
local agent_id="$4"
local agent_name="$5"
local token="$6"
write_file "$KEY_FILE" "$workspace_key"
chmod 600 "$KEY_FILE"
write_file "$TOKEN_FILE" "$token"
chmod 600 "$TOKEN_FILE"
jq -nc \
--arg baseUrl "$base_url" \
--arg workspaceKey "$workspace_key" \
--arg workspaceId "$workspace_id" \
--arg agentId "$agent_id" \
--arg agentName "$agent_name" \
--arg token "$token" \
--arg updatedAt "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
'{
baseUrl: $baseUrl,
workspaceKey: $workspaceKey,
workspaceId: $workspaceId,
agentId: $agentId,
agentName: $agentName,
token: $token,
cli: "codex",
updatedAt: $updatedAt
}' > "$STATE_FILE"
chmod 600 "$STATE_FILE"
}
main() {
run_local_setup
load_env
if ! command_exists curl || ! command_exists jq; then
exit 0
fi
mkdir -p "$RELAY_DIR"
local base_url workspace_key workspace_id agent_name registration token registered_name agent_id
base_url=$(normalize_base_url "${RELAY_BASE_URL:-}")
workspace_key=$(trim "${RELAY_API_KEY:-}")
workspace_id=""
if [ -z "$workspace_key" ] && [ -s "$KEY_FILE" ]; then
workspace_key=$(trim "$(cat "$KEY_FILE" 2>/dev/null || true)")
fi
if [ -z "$workspace_key" ]; then
local created
created=$(create_workspace "$base_url" "$(derive_workspace_name)")
workspace_key=$(json_value "$created" '.api_key')
[ -z "$workspace_key" ] && workspace_key=$(json_value "$created" '.apiKey')
[ -z "$workspace_key" ] && workspace_key=$(json_value "$created" '.data.api_key')
[ -z "$workspace_key" ] && workspace_key=$(json_value "$created" '.data.apiKey')
workspace_id=$(json_value "$created" '.workspace_id')
[ -z "$workspace_id" ] && workspace_id=$(json_value "$created" '.workspaceId')
[ -z "$workspace_id" ] && workspace_id=$(json_value "$created" '.data.workspace_id')
[ -z "$workspace_id" ] && workspace_id=$(json_value "$created" '.data.workspaceId')
fi
[ -z "$workspace_key" ] && exit 0
agent_name=$(derive_agent_name)
# Re-namespace state directory now that we know the agent name
RELAY_DIR="${BASE_RELAY_DIR}/agents/${agent_name}"
KEY_FILE="${RELAY_DIR}/workspace-key"
TOKEN_FILE="${RELAY_DIR}/token"
STATE_FILE="${RELAY_DIR}/codex-session.json"
mkdir -p "$RELAY_DIR"
registration=$(register_v1_agents "$base_url" "$workspace_key" "$agent_name")
if [ -z "$registration" ]; then
registration=$(register_v1_register "$base_url" "$workspace_key" "$agent_name")
fi
[ -z "$registration" ] && exit 0
token=$(json_value "$registration" '.token')
[ -z "$token" ] && token=$(json_value "$registration" '.data.token')
registered_name=$(json_value "$registration" '.name')
[ -z "$registered_name" ] && registered_name=$(json_value "$registration" '.data.name')
agent_id=$(json_value "$registration" '.id')
[ -z "$agent_id" ] && agent_id=$(json_value "$registration" '.agent_id')
[ -z "$agent_id" ] && agent_id=$(json_value "$registration" '.data.id')
[ -z "$agent_id" ] && agent_id=$(json_value "$registration" '.data.agent_id')
[ -z "$workspace_id" ] && workspace_id=$(json_value "$registration" '.workspace_id')
[ -z "$workspace_id" ] && workspace_id=$(json_value "$registration" '.workspaceId')
[ -z "$workspace_id" ] && workspace_id=$(json_value "$registration" '.data.workspace_id')
[ -z "$workspace_id" ] && workspace_id=$(json_value "$registration" '.data.workspaceId')
[ -z "$token" ] && exit 0
[ -z "$registered_name" ] && registered_name="$agent_name"
[ -z "$agent_id" ] && agent_id="$registered_name"
[ -z "$workspace_id" ] && workspace_id="ws_unknown"
persist_state "$base_url" "$workspace_key" "$workspace_id" "$agent_id" "$registered_name" "$token"
}
main "$@"
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
PLUGIN_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
ENV_FILE="${PLUGIN_DIR}/.env"
BASE_RELAY_DIR="${HOME}/.relay"
# Per-agent namespacing: use RELAY_AGENT_NAME to avoid concurrent agents
# overwriting each other's state files
_agent_ns=$(printf '%s' "${RELAY_AGENT_NAME:-}" | tr -c 'A-Za-z0-9._-' '-' | sed 's/^-*//; s/-*$//' | cut -c1-64)
if [ -n "$_agent_ns" ]; then
RELAY_DIR="${BASE_RELAY_DIR}/agents/${_agent_ns}"
else
RELAY_DIR="${BASE_RELAY_DIR}"
fi
TOKEN_FILE="${RELAY_DIR}/token"
STATE_FILE="${RELAY_DIR}/codex-session.json"
DEFAULT_BASE_URL="https://api.relaycast.dev"
EMPTY_OUTPUT='{}'
MAX_RENDERED_MESSAGES=20
load_env() {
if [ -f "$ENV_FILE" ]; then
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
fi
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
trim() {
printf '%s' "${1:-}" | awk '{$1=$1;print}'
}
normalize_base_url() {
local value
value=$(trim "${1:-}")
if [ -z "$value" ] && [ -f "$STATE_FILE" ] && command_exists jq; then
value=$(jq -r '.baseUrl // empty' "$STATE_FILE" 2>/dev/null || true)
fi
value=$(trim "${value:-$DEFAULT_BASE_URL}")
value=${value%/}
printf '%s' "${value:-$DEFAULT_BASE_URL}"
}
json_number() {
local payload="$1"
local query="$2"
printf '%s' "$payload" | jq -r "$query" 2>/dev/null || printf '0'
}
read_stop_hook_active() {
if ! command_exists jq; then
printf 'false'
return
fi
jq -r '.stop_hook_active // false' 2>/dev/null || printf 'false'
}
read_token() {
local token
token=$(trim "${RELAY_TOKEN:-}")
if [ -n "$token" ]; then
printf '%s' "$token"
return
fi
if [ -s "$TOKEN_FILE" ]; then
trim "$(cat "$TOKEN_FILE" 2>/dev/null || true)"
return
fi
printf ''
}
main() {
local input stop_hook_active token base_url messages count formatted overflow reason
load_env
if ! command_exists curl || ! command_exists jq; then
printf '%s\n' "$EMPTY_OUTPUT"
exit 0
fi
input=$(cat)
stop_hook_active=$(printf '%s' "$input" | read_stop_hook_active)
if [ "$stop_hook_active" = "true" ]; then
printf '%s\n' "$EMPTY_OUTPUT"
exit 0
fi
token=$(read_token)
if [ -z "$token" ]; then
printf '%s\n' "$EMPTY_OUTPUT"
exit 0
fi
base_url=$(normalize_base_url "${RELAY_BASE_URL:-}")
messages=$(
curl -fsS \
-X POST \
-H "Authorization: Bearer ${token}" \
-H 'Content-Type: application/json' \
-d '{}' \
"${base_url}/v1/inbox/check" 2>/dev/null || true
)
if [ -z "$messages" ]; then
printf '%s\n' "$EMPTY_OUTPUT"
exit 0
fi
count=$(json_number "$messages" '(.messages // []) | length')
case "$count" in
''|*[!0-9]*) count=0 ;;
esac
if [ "$count" -eq 0 ]; then
printf '%s\n' "$EMPTY_OUTPUT"
exit 0
fi
formatted=$(
printf '%s' "$messages" | jq -r --argjson limit "$MAX_RENDERED_MESSAGES" '
(.messages // [])
| .[:$limit]
| map(
if ((.channel // "") | length) > 0 then
"Relay message from \(.from // "unknown") in #\(.channel)\(if ((.id // "") | length) > 0 then " [\(.id)]" else "" end): \((.text // "") | gsub("[\\r\\n]+"; " "))"
else
"Relay message from \(.from // "unknown")\(if ((.id // "") | length) > 0 then " [\(.id)]" else "" end): \((.text // "") | gsub("[\\r\\n]+"; " "))"
end
)
| join("\n")
' 2>/dev/null || true
)
if [ -z "$formatted" ]; then
printf '%s\n' "$EMPTY_OUTPUT"
exit 0
fi
overflow=""
if [ "$count" -gt "$MAX_RENDERED_MESSAGES" ]; then
overflow=$(printf '\n... and %s more unread relay message(s).' "$((count - MAX_RENDERED_MESSAGES))")
fi
reason=$(printf 'You have %s unread relay message(s). Please read and respond before stopping:\n%s%s' "$count" "$formatted" "$overflow")
jq -nc --arg reason "$reason" '{decision: "block", reason: $reason}'
}
main "$@"
Codex Relay Skill
Codex-native multi-agent coordination via Relaycast.
What it does
This package gives Codex a reusable relay coordination layer so sub-agents can communicate through Relaycast instead of staying limited to parent-only result collection.
It includes:
- a Codex skill that teaches lead and worker messaging protocol
- an MCP dependency declaration for Relaycast
- a template Agent Relay MCP config block for
.codex/config.toml - a
relay-workercustom agent template for.codex/agents/
With these pieces installed, Codex can:
- coordinate teams through direct messages, channels, and threads
- require ACK/DONE signaling from workers
- let workers send peer-to-peer updates through Relaycast
- reuse the same relay workflow across project-scoped and user-scoped setups
Installation
mkdir -p .agents/skills
cp -R plugins/codex-relay-skill .agents/skills/agent-relayThat's it. Everything else is automatic.
On first use, the skill self-installs by running scripts/setup.sh, which:
- adds the Agent Relay MCP server to
.codex/config.toml - enables
features.codex_hooks = true - writes
.codex/hooks.jsonwith SessionStart, UserPromptSubmit, and Stop hooks - installs
.codex/agents/relay-worker.toml
All of this is idempotent — safe to run multiple times, and it merges with existing config rather than overwriting.
For user-wide availability, install to $HOME/.agents/skills/agent-relay instead.
Optional: join an existing workspace
Set RELAY_API_KEY before launching Codex to join a specific Relaycast workspace:
export RELAY_API_KEY="rk_live_your_key_here"If unset, a new workspace is auto-created on the first session.
<details> <summary>Manual setup (advanced)</summary>
If you prefer to configure everything manually instead of using the auto-installer:
1. Add to .codex/config.toml:
features.codex_hooks = true
[mcp_servers.relaycast]
command = "npx"
args = ["-y", "agent-relay", "mcp"]
env = { RELAY_API_KEY = "", RELAY_BASE_URL = "https://api.relaycast.dev", RELAY_AGENT_TYPE = "agent" }2. Copy hooks config:
cp .agents/skills/agent-relay/hooks/hooks.json .codex/hooks.json3. Install the worker agent:
mkdir -p .codex/agents
cp .agents/skills/agent-relay/codex-config/relay-worker.toml .codex/agents/relay-worker.toml</details>
Usage
Use the skill directly
Invoke the skill explicitly:
$agent-relay Coordinate this refactor with two workers and keep all status updates in Relaycast.Or describe the task naturally and let Codex match the skill from its description.
Spawn relay workers
Once relay-worker.toml is installed, delegate bounded tasks to the relay-worker custom agent and include:
- the worker relay name
- the lead relay name
- the workspace-key source
- exact task scope
- completion criteria
Example:
Spawn a relay-worker named api-worker.
Have it check Relaycast, ACK me, update the API route tests only, send STATUS after the first green test run, and send DONE with evidence before exit.Coordinate a team
Use Relaycast when workers need to message each other directly, not only the lead. Good fits:
- parallel implementation across separate subsystems
- lead/worker review loops
- shared channel updates for longer-running tasks
- cross-terminal or cross-machine collaboration
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
RELAY_API_KEY | No | "" in the template config | Relaycast workspace key |
RELAY_BASE_URL | No | https://api.relaycast.dev | Relaycast API base URL |
RELAY_AGENT_TYPE | No | agent | Default Relaycast agent type |
RELAY_AGENT_NAME | No | unset | Optional stable relay identity when your workflow wants a fixed name |
Plugin structure
codex-relay-skill/
SKILL.md # Codex skill manifest and workflow instructions
README.md # Installation and usage docs
agents/
openai.yaml # Agent Relay MCP dependency metadata
codex-config/
config.toml # Template MCP server config for .codex/config.toml
relay-worker.toml # Template custom worker agent for .codex/agents/
scripts/
setup.sh # Auto-installer (runs on first skill activation)
hooks/
hooks.json # Hook definitions (SessionStart, Stop, UserPromptSubmit)
session-start.sh # Auto-connect and state persistence
stop-inbox.sh # Block exit while unread messages exist
prompt-inbox.sh # Rate-limited inbox polling and context injectionInstalled layout in a project typically looks like:
.agents/skills/agent-relay/ # Skill directory Codex scans
.codex/config.toml # Runtime Agent Relay MCP server + features.codex_hooks
.codex/hooks.json # Hook wiring (copied from skill)
.codex/agents/relay-worker.toml#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
SKILL_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
TARGET_ROOT="${1:-$PWD}"
CODEX_DIR="${TARGET_ROOT}/.codex"
AGENTS_DIR="${CODEX_DIR}/agents"
CONFIG_FILE="${CODEX_DIR}/config.toml"
HOOKS_FILE="${CODEX_DIR}/hooks.json"
WORKER_SOURCE="${SKILL_DIR}/codex-config/relay-worker.toml"
WORKER_TARGET="${AGENTS_DIR}/relay-worker.toml"
command_exists() {
command -v "$1" >/dev/null 2>&1
}
shell_quote() {
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
}
write_if_changed() {
local path="$1"
local tmp="$2"
if [ -f "$path" ] && cmp -s "$path" "$tmp"; then
rm -f "$tmp"
return
fi
mv "$tmp" "$path"
}
ensure_features_codex_hooks() {
local file="$1"
local tmp
tmp=$(mktemp)
awk '
BEGIN {
in_features = 0
features_seen = 0
codex_hooks_written = 0
}
function write_codex_hooks() {
if (!codex_hooks_written) {
print "codex_hooks = true"
codex_hooks_written = 1
}
}
{
if ($0 ~ /^[[:space:]]*features[.]codex_hooks[[:space:]]*=/) {
print "features.codex_hooks = true"
codex_hooks_written = 1
next
}
if ($0 ~ /^\[[^]]+\][[:space:]]*$/) {
if (in_features) {
write_codex_hooks()
}
if ($0 == "[features]") {
in_features = 1
features_seen = 1
} else {
in_features = 0
}
print
next
}
if (in_features && $0 ~ /^[[:space:]]*codex_hooks[[:space:]]*=/) {
write_codex_hooks()
next
}
print
}
END {
if (in_features) {
write_codex_hooks()
}
if (!features_seen && !codex_hooks_written) {
if (NR > 0) {
print ""
}
print "[features]"
print "codex_hooks = true"
}
}
' "$file" > "$tmp"
write_if_changed "$file" "$tmp"
}
ensure_top_level_approval_policy() {
local file="$1"
local tmp
tmp=$(mktemp)
awk '
BEGIN {
found = 0
first_section = 0
inserted = 0
}
{
# If we find approval_policy at top level (before any section or as dotted key), mark found
if (!first_section && $0 ~ /^[[:space:]]*approval_policy[[:space:]]*=/) {
found = 1
print
next
}
# Detect first section header
if ($0 ~ /^\[[^]]+\][[:space:]]*$/) {
if (!first_section && !found && !inserted) {
# Insert approval_policy before the first section header
print "approval_policy = \"on-request\""
print ""
inserted = 1
}
first_section = 1
}
print
}
END {
# File has no sections at all
if (!found && !inserted) {
if (NR > 0) {
print ""
}
print "approval_policy = \"on-request\""
}
}
' "$file" > "$tmp"
write_if_changed "$file" "$tmp"
}
ensure_relaycast_mcp_block() {
local file="$1"
local tmp
tmp=$(mktemp)
awk '
BEGIN {
in_block = 0
block_seen = 0
dotted_seen = 0
command_seen = 0
args_seen = 0
env_seen = 0
command_line = "command = \"npx\""
args_line = "args = [\"-y\", \"agent-relay\", \"mcp\"]"
env_line = "env = { RELAY_API_KEY = \"\", RELAY_BASE_URL = \"https://api.relaycast.dev\", RELAY_AGENT_TYPE = \"agent\" }"
}
function write_missing_keys() {
if (!command_seen) {
print command_line
command_seen = 1
}
if (!args_seen) {
print args_line
args_seen = 1
}
if (!env_seen) {
print env_line
env_seen = 1
}
}
{
if (!in_block && $0 ~ /^[[:space:]]*mcp_servers[.]relaycast[.]command[[:space:]]*=/) {
block_seen = 1
dotted_seen = 1
command_seen = 1
command_line = $0
sub(/^[[:space:]]*mcp_servers[.]relaycast[.]command[[:space:]]*=/, "command =", command_line)
print
next
}
if (!in_block && $0 ~ /^[[:space:]]*mcp_servers[.]relaycast[.]args[[:space:]]*=/) {
block_seen = 1
dotted_seen = 1
args_seen = 1
args_line = $0
sub(/^[[:space:]]*mcp_servers[.]relaycast[.]args[[:space:]]*=/, "args =", args_line)
print
next
}
if (!in_block && $0 ~ /^[[:space:]]*mcp_servers[.]relaycast[.]env[[:space:]]*=/) {
block_seen = 1
dotted_seen = 1
env_seen = 1
env_line = $0
sub(/^[[:space:]]*mcp_servers[.]relaycast[.]env[[:space:]]*=/, "env =", env_line)
print
next
}
if ($0 ~ /^\[[^]]+\][[:space:]]*$/) {
if (in_block) {
write_missing_keys()
}
if ($0 == "[mcp_servers.relaycast]") {
in_block = 1
block_seen = 1
} else {
in_block = 0
}
print
next
}
if (in_block) {
if ($0 ~ /^[[:space:]]*command[[:space:]]*=/) {
command_seen = 1
print
next
}
if ($0 ~ /^[[:space:]]*args[[:space:]]*=/) {
args_seen = 1
print
next
}
if ($0 ~ /^[[:space:]]*env[[:space:]]*=/) {
env_seen = 1
print
next
}
}
print
}
END {
if (in_block) {
write_missing_keys()
}
if (!block_seen && !dotted_seen) {
if (NR > 0) {
print ""
}
print "[mcp_servers.relaycast]"
print command_line
print args_line
print env_line
}
}
' "$file" > "$tmp"
write_if_changed "$file" "$tmp"
}
desired_hooks_json() {
local session_cmd prompt_cmd stop_cmd
session_cmd="bash $(shell_quote "${SKILL_DIR}/hooks/session-start.sh")"
prompt_cmd="bash $(shell_quote "${SKILL_DIR}/hooks/prompt-inbox.sh")"
stop_cmd="bash $(shell_quote "${SKILL_DIR}/hooks/stop-inbox.sh")"
jq -n \
--arg session_cmd "$session_cmd" \
--arg prompt_cmd "$prompt_cmd" \
--arg stop_cmd "$stop_cmd" \
'{
hooks: {
SessionStart: [
{
hooks: [
{
type: "command",
command: $session_cmd,
timeoutSec: 15,
statusMessage: "Connecting Relaycast"
}
]
}
],
UserPromptSubmit: [
{
hooks: [
{
type: "command",
command: $prompt_cmd,
timeoutSec: 5,
statusMessage: "Checking relay inbox"
}
]
}
],
Stop: [
{
hooks: [
{
type: "command",
command: $stop_cmd,
timeoutSec: 5,
statusMessage: "Verifying relay inbox"
}
]
}
]
}
}'
}
merge_hooks_file() {
local desired_json="$1"
local tmp
tmp=$(mktemp)
if [ -f "$HOOKS_FILE" ] && jq empty "$HOOKS_FILE" >/dev/null 2>&1; then
jq \
--argjson desired "$desired_json" \
'
def remove_owned_groups($event; $status; $script):
.hooks[$event] = (
(.hooks[$event] // [])
| map(
select(
(
(.hooks // [])
| any(
((.statusMessage // "") == $status) or
(((.command // "") | tostring) | contains($script))
)
) | not
)
)
);
.hooks = (.hooks // {})
| remove_owned_groups("SessionStart"; "Connecting Relaycast"; "session-start.sh")
| remove_owned_groups("UserPromptSubmit"; "Checking relay inbox"; "prompt-inbox.sh")
| remove_owned_groups("Stop"; "Verifying relay inbox"; "stop-inbox.sh")
| .hooks.SessionStart = ((.hooks.SessionStart // []) + $desired.hooks.SessionStart)
| .hooks.UserPromptSubmit = ((.hooks.UserPromptSubmit // []) + $desired.hooks.UserPromptSubmit)
| .hooks.Stop = ((.hooks.Stop // []) + $desired.hooks.Stop)
' "$HOOKS_FILE" > "$tmp"
else
printf '%s\n' "$desired_json" > "$tmp"
fi
write_if_changed "$HOOKS_FILE" "$tmp"
}
install_worker_agent() {
if [ ! -f "$WORKER_SOURCE" ]; then
return
fi
local tmp
tmp=$(mktemp)
cp "$WORKER_SOURCE" "$tmp"
write_if_changed "$WORKER_TARGET" "$tmp"
}
main() {
if ! command_exists jq; then
exit 0
fi
mkdir -p "$CODEX_DIR" "$AGENTS_DIR"
touch "$CONFIG_FILE"
chmod +x "${SKILL_DIR}/scripts/setup.sh" "${SKILL_DIR}/hooks/"*.sh 2>/dev/null || true
ensure_features_codex_hooks "$CONFIG_FILE"
ensure_top_level_approval_policy "$CONFIG_FILE"
ensure_relaycast_mcp_block "$CONFIG_FILE"
merge_hooks_file "$(desired_hooks_json)"
install_worker_agent
}
main "$@"
Related skills
FAQ
What does agent-relay set up on first activation?
It runs scripts/setup.sh to add the Agent Relay MCP server to .codex/config.toml, enable hooks, install hooks.json, and copy the relay-worker.toml agent definition.
What are the two kinds of workers?
Relaycast workspace agents (add_agent) for messaging-heavy tasks, and Codex sub-agents (spawn_agent with relay-worker) for code-heavy tasks.