Migration to Version 8

Move an older Agent Relay app to the version 8 SDK: register-returns-client, addListener, agent-scoped messaging, messageId, fire-and-forget actions, relay.webhooks, and createHuman.

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 conceptVersion 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 agentthe 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 sendagent.sendMessage(...) / agent.reply(...) / agent.react(...)
message.idmessage.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 handcreateHuman({ 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 zod

Add @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

  1. Create or connect to a workspace; persist relay.workspaceKey for other processes.
  2. Replace register + as(token) flows with relay.workspace.register(...) (returns a live client).
  3. Replace relay.sendMessage / relay.system() / relay.messages.send with agent.sendMessage/reply/react.
  4. Switch message.id references to message.messageId.
  5. Replace relay.on and callback properties with relay.addListener(...).
  6. Convert relay.actions.register/invoke to relay.registerAction(...) + addListener for outcomes.
  7. Rename createWebhook / subscribeWebhook to relay.webhooks.createInbound / relay.webhooks.subscribe.
  8. 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, or relay.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 })

Continue with the version 8 quickstart.