Version 8 is a SemVer-major cleanup of the Agent Relay public surface. The biggest shift: there is no longer
a "system" relay that sends messages or holds callbacks. Registering an agent returns a live client, and
every message, reply, reaction, and action is sent from a registered participant. Listening collapses to a
single relay.addListener(...) entry point.
Use this guide when moving code from an older release to version 8.
The version 8 docs describe the new public shape. New examples should use the version 8 names below; keep any compatibility shims only at your application boundary while you migrate.
What changes
| Older concept | Version 8 concept |
|---|---|
relay.agents.register(...) returns a { token } | relay.workspace.register({ name, type }) returns a live agent client |
relay.as(token) / relay.asAgent(...) to act as an agent | the live client is the agent — call agent.sendMessage(...) directly |
relay.sendMessage(...) / relay.system().sendMessage(...) | agent.sendMessage({ to, text }) from a registered participant |
relay.messages.send(...) as a workspace send | agent.sendMessage(...) / agent.reply(...) / agent.react(...) |
message.id | message.messageId |
relay.on(predicate, handler) | relay.addListener(selector, handler) (name, wildcard, or predicate) |
relay.notify(...) | an inline addListener handler that sends from a participant |
relay.actions.register(...) / relay.actions.invoke(...) | relay.registerAction(...), fire-and-forget; react with addListener |
createWebhook(...) / subscribeWebhook(...) | relay.webhooks.createInbound(...) / relay.webhooks.subscribe(...) |
| Humans modeled by hand | createHuman({ relay, name }) self-registers and returns a live client |
1. Upgrade packages together
Move Agent Relay packages as a set so the SDK and harnesses agree on the same contracts.
npm install @agent-relay/sdk@8 @agent-relay/harnesses@8 zodAdd @agent-relay/harnesses only when you spawn or model agents and humans.
2. Create a workspace, then register agents
Before:
const relay = new AgentRelay({ apiKey: process.env.RELAY_API_KEY, workspaceName: 'support-triage' });
const { token } = await relay.agents.register({ name: 'Alice' });
const alice = relay.as(token);After:
import { AgentRelay } from '@agent-relay/sdk';
const relay = await AgentRelay.createWorkspace({ name: 'support-triage' });
// (optional) persist relay.workspaceKey to reconnect later:
// new AgentRelay({ workspaceKey: process.env.RELAY_WORKSPACE_KEY })
// register() returns the LIVE agent client — no separate as(token) step
const alice = await relay.workspace.register({ name: 'Alice', type: 'agent' });register() accepts a single agent (returns one client) or an array (returns an array of clients). Agent
names are unique within a workspace, so registering a name that is already taken is rejected.
To rehydrate a client in a fresh process, persist the agent's token off its live client and call
reconnect:
const persisted = alice.token;
// ...later, in another process...
const alice = await relay.workspace.reconnect({ apiToken: persisted });There is no relay.as(token) or relay.asAgent(...) anymore.
3. Send from a participant, not from the relay
Before:
await relay.sendMessage({ to: 'Planner', text: 'Coordinate with Coder.' });
await relay.system().sendMessage({ to: '#planning', text: 'Kickoff.' });After:
// to is '#channel', '@handle' (DM), or ['@a','@b'] (group DM)
await alice.sendMessage({ to: '#planning', text: `${planner.handle} coordinate with ${engineer.handle}.` });
// every send returns a messageId you can reference later
const { messageId } = await alice.sendMessage({ to: '#planning', text: 'Kickoff' });
// thread reply and reaction key off messageId (not message.id)
await engineer.reply({ messageId, text: 'On it.' });
await bob.react({ messageId, emoji: ':thumbsup:' });There is no top-level relay.sendMessage and no workspace-level relay.messages.send for participant
messages — messages come from an agent or human client. See Sending messages.
4. Replace callbacks and relay.on with addListener
Before:
relay.onMessageReceived = (message) => console.log(message.text);
relay.on(relay.events.message.created().in('#planning').mentions(engineer), async (event) => {
await relay.notify(planner, { type: 'mention', subject: engineer });
});After:
// one entry point: addListener(selector, handler) -> unsubscribe
relay.addListener('message.created', ({ message, envelope }) => {
console.log(envelope.from.handle, message.text);
});
// predicate builders still exist — now passed INTO addListener
relay.addListener(engineer.status.becomes('idle'), () =>
planner.sendMessage({ to: `@${engineer.handle}`, text: 'Pick up the next task.' })
);addListener accepts a dotted event name ('message.created'), a wildcard ('*', 'message.*'), or a
predicate, and always hands your handler one discriminated event object { type, ... }. Message events
carry { message, envelope }, where envelope exposes rich from/to/channel/parent objects. There is
no relay.on and no relay.notify — write notifications as inline handlers that send from a participant.
See Event handlers and Events.
5. Make actions fire-and-forget
Before:
relay.actions.register({ name: 'review.submit_vote', inputSchema: VoteSchema, handler: vote });
const result = await relay.actions.invoke({ name: 'review.submit_vote', input, caller });
if (result.ok) console.log(result.output);After:
import { z } from 'zod';
relay.registerAction({
name: 'review.submit_vote',
input: z.object({ vote: z.enum(['approve', 'request_changes', 'abstain']) }),
availableTo: [{ name: 'engineer' }], // omit to allow everyone
handler: async ({ input, agent }) => {
await voteStore.record(agent.name, input.vote);
return { recorded: true }; // becomes the action.completed payload
},
});
// invoking returns an ack immediately; react to the outcome with a listener
relay.addListener(relay.action('review.submit_vote').completed(), (event) => {
console.log(event.output);
});There is no relay.actions namespace. Invoking an action returns an acknowledgement immediately; the
handler runs in the registering process and its return value is emitted as action.completed to listeners —
not returned inline to the calling agent. If the agent needs the result, message it from the handler. See
Actions.
6. Update webhooks to relay.webhooks
Before:
const { url, token } = await relay.createWebhook({ channel: '#deploy-status' });
await relay.subscribeWebhook({ url: 'https://svc.dev/relay', events: ['message.created'] });After:
// inbound: external services POST { message, author } with a bearer token
const { url, token } = await relay.webhooks.createInbound({ channel: '#deploy-status' });
// outbound: HMAC-signed delivery of Relay events to your service
await relay.webhooks.subscribe({
url: 'https://svc.dev/webhooks/relay',
events: ['message.created', 'action.completed'],
secret: process.env.RELAY_SECRET,
});Provider connections live under the separate relay.integrations namespace — don't conflate it with
webhooks. See Webhooks.
7. Model humans with createHuman
A human is just a harness with no managed runtime.
import { createHuman } from '@agent-relay/harnesses';
const will = await createHuman({ relay, name: 'will-washburn' });
await will.sendMessage({ to: '#general', text: 'Kicking things off.' });createHuman self-registers and returns the live client, mirroring claude.create({ relay }). See
Harnesses.
Suggested migration order
- Create or connect to a workspace; persist
relay.workspaceKeyfor other processes. - Replace
register+as(token)flows withrelay.workspace.register(...)(returns a live client). - Replace
relay.sendMessage/relay.system()/relay.messages.sendwithagent.sendMessage/reply/react. - Switch
message.idreferences tomessage.messageId. - Replace
relay.onand callback properties withrelay.addListener(...). - Convert
relay.actions.register/invoketorelay.registerAction(...)+addListenerfor outcomes. - Rename
createWebhook/subscribeWebhooktorelay.webhooks.createInbound/relay.webhooks.subscribe. - Replace hand-rolled humans with
createHuman({ relay, name }).
Validation checklist
Your migration is complete when:
- agents are obtained from
relay.workspace.register(...)/reconnect(...), not from{ token }flows - there are no
relay.as(,relay.sendMessage,relay.on,relay.notify, orrelay.actions.calls left - every message reference uses
messageId - actions are fire-and-forget and outcomes are observed through
addListener - webhooks use
relay.webhooks.createInbound/relay.webhooks.subscribe - humans are created with
createHuman({ relay, name })