# Relays
A Relay is a fixed chain of agents you run from anywhere: JSON in, agents and
scripts do the work, JSON out. You describe what you want in a Builder chat,
test the draft, then publish versioned releases (`v1`, `v2`, …) that stay
stable while you keep editing.
## How one call flows
1. CallPOST your function, input JSON, and an idempotency key. Empty version means the active release.
→
2. Run pinnedThe run executes that release's frozen snapshot — later publishes can't change it mid-flight.
→
3. Poll resultGET the run until it completes. The response carries the JSON result plus the version that ran.
Repeat an idempotency key and you get the original run back (`duplicate:
true`), even after newer releases publish. Reuse a key with *different* input
and the API refuses with 409 — keys are promises, not suggestions.
## Drafts vs releases
| | Draft | Release (v1, v2, …) |
|---|---|---|
| Lives in | Your editable workspace | A frozen, checksummed snapshot |
| Changes when | You edit in the Builder | Never — publish mints a new one |
| Runs via | Builder tests | API calls (active, or a pinned older one) |
| On failure | You see it in chat and fix it | Fails visibly with the step and reason |
Publishing requires a valid graph: a final output agent producing JSON, at
least one enabled function with a required object `INPUT`, and saved code for
every script step. Only owners and editors can publish or call.
## What's inside a Relay
Agents
Authored prompts that read the caller's JSON ({{input}}), use tools and skills, and must return valid JSON.
Scripts
Strict Python steps with saved code. A script failure stops the run — no retries, no repairs.
Branches
Deterministic routes on JSON values. Every route must reach the output agent; no loops or joins.
Schedules can fire a Relay on a timer with a fixed JSON payload. Slack
notifications are supported; WhatsApp, bot chats, and Pulse are not.
## API reference
All calls are authenticated the same way as the rest of the API.
**Start a run** — `POST /api/relays/{id}/runs`
```json
{
"function": "greet",
"input": { "name": "Ada" },
"idempotency_key": "order-8842-attempt-1",
"version": "v1"
}
```
`version` is optional and defaults to the active release. Returns `202`:
```json
{
"run_id": "b28e48e9-…",
"status": "running",
"version": "v1",
"duplicate": false,
"poll_url": "/api/relays/wf_bb882752/runs/b28e48e9-…"
}
```
**Poll a run** — `GET /api/relays/{id}/runs/{run}`
Returns the run status while it works, and the output agent's JSON plus
`version` once complete. Unknown runs and other people's runs both answer
404 — the API never confirms what you may not see.
**List releases** — `GET /api/relays/{id}/releases`
Returns the active version and every published release with its content hash.
## Limits to know
- Snapshots hold UTF-8 text only (50 MiB / 5000 files). Binary assets are
outside the release contract.
- Schedules execute the draft, not a pinned release.
- A crashed run ends honestly as `interrupted` and stays pollable — but it
never resumes mid-chain. Don't describe Relays as resumable.
- A release whose files change after publishing fails its checksum and stops
serving until you republish. Runs should only write inside their run folder.
Related: [Security overview](../security/README.md),
[Sharing and slots](../security/sharing.md).