A workspace contains agents — the participants that converse — and nodes — the places that do work. A node is an enrolled context on a machine: usually a project directory, sometimes an application. One machine can host several nodes, and the machine itself is just an attribute used for grouping and placement.
Processes called providers attach to a node and give it abilities. Every provider connects directly to the engine with the node's token and registers what it offers.
The model
| Term | What it is |
|---|---|
| Node | An enrolled context in a workspace — a project directory or an application. The unit others target: "invoke run-etl on data-pipeline." Multiple nodes per machine. |
| Provider | A process attached to a node. It connects directly to the engine, registers a subset of the node's capabilities, and executes their invokes. |
| Capability | A named unit a provider registers with the engine — an action with a handler, or spawn/release capacity. Always node-scoped. |
| Machine | An attribute on the node record, used for grouping fleet views and as an input to placement. Never a capability scope. |
Node identity is derived from the machine, the working directory, and the workspace, so the same
directory stays distinct across workspaces and several nodes coexist on one machine. All providers
on a node share the node's nt_live_* token; each declares its own identity — a stable name and
a per-connection instance_id — when it connects.
Capacity and actions
Providers register two kinds of capability.
- The node's agent runtime (a Rust provider) runs agents. It registers capacity — which
harnesses it can spawn (
spawn:claude,spawn:codex) and how many. Capacity feeds placement and spawn delegation; it is never materialized as an invokable action. - Capability providers — written in TypeScript, Python, or Swift — register actions: named
handlers like
run-etlorscreenshotthat anything in the workspace can invoke. The engine materializes an action and dispatches its invokes to the registering provider.
Action names are unique within a node: two providers cannot both register run-etl on the same
node, and the duplicate is rejected at startup. Two different nodes may each define their own
run-etl. Spawn capacity is not exclusive — several providers advertising spawn:claude is simply
more capacity, aggregated at the node level.
Invoking a capability
An invoke is addressed to a node. The engine routes it down the socket of the provider that registered the action, the handler runs on that machine, and the result returns to the caller.
curl "$RELAY_BASE_URL/v1/nodes/data-pipeline/actions/run-etl/invoke" \
-H "Authorization: Bearer $RELAY_AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "date": "2026-07-07" }'Agents invoke the same capability through their MCP tools — the engine generates one tool per
action from its input schema. Like every action, an invoke is fire-and-forget:
the caller gets an acknowledgement ({ invocationId }), and the handler's return value reaches
listeners as action.completed. An invoke whose provider is offline fails fast by default; a
capability can opt into queueing until its provider returns.
Authoring a capability provider
A node's TypeScript capabilities live in an agent-relay.ts file, defined with
@agent-relay/fleet. Each capability is keyed by name; action(...) builds a handler that
receives the validated input and a handler context.
import { defineNode, action } from '@agent-relay/fleet';
import { z } from 'zod';
export default defineNode({
name: 'data-pipeline',
capabilities: {
'run-etl': action({ input: z.object({ date: z.string() }) }, async (input, ctx) => {
const summary = await runEtl(input.date);
await ctx.sendMessage({ to: '#data', text: `ETL done for ${input.date}.` });
return summary;
}),
},
});Python and Swift providers register the same way against their SDK's node client and connect on their own — a long-running app or a launchd/systemd service reads the node credential from the enrollment and serves directly, with no start-order dependency on any other provider.
from agent_relay.node import NodeProvider
node = NodeProvider.from_enrollment() # reads the node token + base URL
@node.capability("run-etl", description="Run the ETL job")
async def run_etl(input, ctx): ...
node.serve()The handler context (ctx) exposes engine helpers — sendMessage, spawnAgent — that call the
engine with the node token. A Python capability that spawns an agent does so through the engine's
placement, which lands on an agent runtime like any other spawn; nothing tunnels through a local
process.
Liveness is per provider
A node is online when at least one provider is connected. A capability is live only while its owning provider's connection is up and that provider's heartbeat still lists it. Liveness is therefore per provider: if the Python process is down, its actions are unavailable while agents keep running on the agent runtime, and the reverse holds too.
Wrapping spawn
Spawning composes with actions. Registering an action named spawn:<harness> shadows the node's
native spawn capacity for that harness: inbound spawn invokes go to the handler, which can mutate
the command, environment, or working directory before delegating to the agent runtime — and audit
or announce after. A handler in any language becomes before/after policy around spawn.
@node.capability("spawn:claude") # shadows the node's native spawn:claude
async def spawn_claude(input, ctx):
agent = input["agent"]
agent["cli"] = f"claude --permission-mode plan {agent.get('args', '')}"
result = await ctx.spawn_agent(agent) # delegates to the node's agent runtime
await ctx.send_message(to="ops", text=f"spawned {result['name']}")
return resultctx.spawn_agent addresses the node's capacity directly, so a shadow handler that delegates never
re-enters itself. While a spawn:<harness> shadow is registered, the native capacity for that
harness is reachable only through the shadow — the wrapper cannot be silently bypassed, even when
its provider is offline.
Getting a machine into the fleet
Bringing a node online is two commands. Enroll once per context — this redeems a one-time token and persists the node credential — then start the node in the project.
agent-relay cloud enroll --token ocl_node_enr_...
agent-relay node upnode up brings the current context's node online: it starts the node's agent runtime and serves
the project's capability definitions as further providers, each connecting directly to the engine
with the enrolled node token. Long-running apps skip node up and serve their own node directly
through an SDK.
Nodes
Provider registration, node-addressed invoke, placement, and the node API.
Actions
The fire-and-forget lifecycle, descriptor shape, and invoking over MCP.
Architecture
How delivery, providers, and spawn flow through the engine.
Broker lifecycle
Enroll a node and bring its runtime online from the CLI.