Every step is a save point.
Relay records a step’s result the moment it returns. Crashes, deploys and evicted pods resume from the last checkpoint, and completed steps hand back their stored output instead of running twice.
Relay is a durable workflow engine for AI agents and long‑running jobs. Write steps in TypeScript or Python; Relay checkpoints each one, retries the flaky ones, waits days for a human when it has to, and traces all of it. When a worker dies at 3 a.m., the run doesn’t.
MIT-licensed SDKs · Cloud or self-hosted · No card to start
Animated product demo: a TypeScript workflow called research-agent is typed into an editor and deployed. Its run graph then executes: the search step succeeds, the summarize step fails with a rate-limit error, retries after a two second backoff and succeeds, the review step pauses until a person clicks Approve, and the publish step completes the run.
Running in production at teams who deleted their retry loops
Your agent calls a model, the model calls a tool, the tool calls an API that times out on the fourth try — and somewhere in there, a deploy rolls your pods. Relay turns each of those calls into a step and saves it the moment it returns. The next attempt starts where the last one fell over, not at the top of the file.
02Principles
Three guarantees, each with the mechanism behind it. No magic, just a very stubborn state machine.
Relay records a step’s result the moment it returns. Crashes, deploys and evicted pods resume from the last checkpoint, and completed steps hand back their stored output instead of running twice.
Every run, step, retry and model call is a span — inputs, outputs and token counts included. It exports OpenTelemetry, so it lands in the dashboards you already stare at.
Call approval() and the run parks itself: no worker held, no compute billed. Approve from email, the CLI, the API or a signed webhook, and decide ahead of time what happens when nobody answers.
Refund €1,240.00 for order #88213?
relay approve run_8fK2Lm Run parked. No worker is holding it.
03The SDK
No DSL, no YAML, no drag-and-drop canvas. Wrap anything that talks to the outside world in step() and Relay writes down what happened. On replay, it reads the receipt instead of calling the API again.
step(id, fn)Runs once, stores the result, retries on throw.sleep("7d")Pause for seconds or months. The process exits; the run keeps its place.run.approval(id)Park until a human says yes, no, or nothing at all.step.map(id, xs, fn)Fan out with concurrency limits and per-item retries.schedule: cronCron with IANA time zones. DST included — see the changelog for our apology.import { workflow, step, sleep } from "@relay/sdk";
import { billing, reports } from "./lib";
export const reconcile = workflow({
id: "reconcile-invoices",
schedule: { cron: "0 3 * * *", tz: "Europe/Helsinki" },
timeout: "2h",
}, async ({ run }) => {
const unpaid = await step("list-unpaid", () =>
billing.invoices.list({ status: "unpaid" }));
const chased = await step.map("chase", unpaid, {
concurrency: 20,
retries: { max: 5, backoff: "exponential" },
}, (inv) => billing.remind(inv.id));
if (chased.failed.length > 0) {
await run.approval("escalate", {
notify: ["email:finance@acme.dev", "webhook:ops"],
payload: chased.failed,
timeout: "24h",
});
}
// Yes, a week. The process can exit; the run can't.
await sleep("7d");
return step("report", () => reports.weekly(chased));
});from relay import workflow, step, sleep, Run
from .lib import billing, reports
@workflow(
id="reconcile-invoices",
schedule={"cron": "0 3 * * *", "tz": "Europe/Helsinki"},
timeout="2h",
)
async def reconcile(run: Run) -> dict:
unpaid = await step("list-unpaid", billing.invoices.list,
status="unpaid")
chased = await step.map(
"chase", unpaid, billing.remind,
concurrency=20,
retries=step.Retries(max=5, backoff="exponential"),
)
if chased.failed:
await run.approval(
"escalate",
notify=["email:finance@acme.dev", "webhook:ops"],
payload=chased.failed,
timeout="24h",
)
# Yes, a week. The process can exit; the run can't.
await sleep("7d")
return await step("report", reports.weekly, chased)04Traces
Tracing is on by default and speaks OpenTelemetry. Click a span for inputs, outputs, retries and exactly what the model said, then replay the run on your laptop from that point, with recorded results instead of live calls.
05Built for failure
Workers die, deploys roll, spot instances vanish mid-sentence. Relay leases each run to one worker and checkpoints after every step. If the lease lapses, another worker picks up at the last checkpoint. No double charges, no duplicate emails, no 3 a.m. archaeology.
standby · polling for work
standby · polling for work
Press Replay run to start, then Kill worker while a step is in flight.
06Numbers
The scheduler is the part you shouldn’t have to think about, so we think about it constantly. Measured across all regions and published on the status page, including the bad days.
p99 scheduling latency
38ms
From step enqueued to step running on a worker. Last 30 days, all regions.
Steps run in August
2.1B
About 810 a second, around the clock. Retries count; sleeping doesn’t.
2,143,882,019
steps since Aug 1 · counting live
API & scheduler uptime
99.995%
Trailing 90 days. One partial outage, written up in public.
07Pricing
Sleeping for a week, waiting on an approval, backing off from a rate limit: none of it is billed. You pay when your code runs, and retries count as the steps they are.
≈ 6,900 runs a day of a 12-step agent.
For side projects and 2 a.m. experiments.
At 2.5M, you’d hit the limit on day 1.
Start on HobbyFor teams with a pager and a budget line.
$49 base with 1M steps, + 1.5M × $18/M
Start a 14-day trialFor when your steps have their own finance team.
Volume pricing from 50M steps.
Talk to an engineer08Changelog
We release on Tuesdays and write the notes on Wednesdays, once we’ve seen what broke.
Approve or reject from email, the CLI (relay approve run_…), the API, or a signed webhook. Timeouts can now escalate to a second approver instead of failing the run.
yield partial results from a step and Relay checkpoints each chunk, so a crash mid-stream resumes at the last chunk instead of the first token.
Including Europe/Helsinki, where our own 03:00 cleanup job silently skipped March 29th. Sorry, Helsinki. Schedules now fire once, at the wall-clock time you wrote.
relay replay: rerun any production run on your laptopPulls the recorded step results, so the run replays deterministically without calling a single external API. Set a breakpoint at step four and step through.
Limit to ten concurrent calls per customer, not per workflow. Your rate-limited vendor will notice the difference before you do.
09Start
Two commands to a running workflow on your laptop. Deploy the same code to Relay Cloud or your own cluster when you’re ready. The free tier doesn’t expire.