NEWHuman approvals are GA v2.4

Code that finishes
what it started.

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.

Start building

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.

↑ A real run, compressed: one rate limit, one retry, one human, zero pages.Approval wait fast‑forwarded · compute billed only while steps run

Running in production at teams who deleted their retry loops

kestrel TALLYFOLD Obelisk parcelwise OXBOW driftline LatticeHealth nine/fold

01Why steps

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.

checkpoint after every stepexactly-once results, at-least-once executionno DSL, no YAML

02Principles

We can’t stop your code from failing. We can make it not matter.

Three guarantees, each with the mechanism behind it. No magic, just a very stubborn state machine.

Durable01

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.

Observable02

See the run, not just the logs.

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.

Human-in-the-loop03

Pause for a human. For days, if that’s what it takes.

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.

emailrelay approveRESTwebhooktimeout → escalate
Approval requestedrefund-agent · run_8fK2Lmexpires in 47:59:58

Refund €1,240.00 for order #88213?

Agent’s reasoningCustomer was charged twice on 12 Sep (txn …4471 and …4472). The ledger confirms the duplicate, and the amount is within policy for a first occurrence.
or relay approve run_8fK2Lm
  1. classify✓ 0.8s
  2. check-ledger✓ 1.1s
  3. approvalwaiting · 0 cpu
  4. issue-refundqueued
  5. email-customerqueued

Run parked. No worker is holding it.

03The SDK

Workflows are functions. Steps are receipts.

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.
Read the SDK reference
workflows/reconcile.ts
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));
});
npm i @relay/sdk · Node 20+, Bun, Denoruns nightly at 03:00 Helsinki

04Traces

Every step, attempt and token on one timeline.

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.

Runs / research-agent / run_7Hk2Qp
Completed 8.42s active · 4m 02s paused · 12 spans

05Built for failure

Go on.
Kill the worker.

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.

reindex-knowledge-baserun_9Qe4TzQueued0.0s
  1. 01a
    crawlqueued
  2. 02a
    chunkqueued
  3. 03a
    embedqueued
  4. 04a
    dedupequeued
  5. 05a
    upsertqueued
  6. 06a
    notifyqueued
worker-apool-eu-1 · pid 4412

standby · polling for work

steps run 0312 MB
Checkpoint storeno lease
  1. No checkpoints yet. Each finished step lands here.
worker-bpool-eu-2 · pid 9108

standby · polling for work

steps run 0288 MB

Press Replay run to start, then Kill worker while a step is in flight.

Completed steps re-run
0
In-flight steps restarted
0
Restored from checkpoint
0

06Numbers

Boring numbers, on purpose.

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

Pay for steps. Waiting is free.

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.

2.5Msteps

≈ 6,900 runs a day of a 12-step agent.

Hobby

For side projects and 2 a.m. experiments.

$0/ month

At 2.5M, you’d hit the limit on day 1.

Start on Hobby
  • 50,000 steps a month
  • 3-day trace retention
  • 10 concurrent runs
  • Community forum support

Pro Your range

For teams with a pager and a budget line.

$76/ month

$49 base with 1M steps, + 1.5M × $18/M

Start a 14-day trial
  • 1M steps included, then $18 per million ($10 past 20M)
  • 30-day traces, replay any run locally
  • Approvals by email, CLI, API and webhook
  • 1,000 concurrent runs · alerts to your pager

Enterprise

For when your steps have their own finance team.

Custom

Volume pricing from 50M steps.

Talk to an engineer
  • Workers in your own cloud, state in ours or yours
  • SAML SSO, SCIM, audit log export
  • 99.99% uptime SLA with real credits
  • A named engineer, not a ticket queue
A step is one execution of a step() call. Retries are steps. Sleeps and waits are not.Prices in USD, excl. VAT. Open-source SDKs are free forever.

08Changelog

Shipped, then written down.

We release on Tuesdays and write the notes on Wednesdays, once we’ve seen what broke.

  1. v2.4.0
    Feature

    Human approvals are generally available

    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.

  2. relay-py 1.8.0
    SDK

    Python: async generators as streaming steps

    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.

  3. v2.3.2
    Fix

    Cron schedules respect DST in every IANA zone

    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.

  4. v2.3.0
    Feature

    relay replay: rerun any production run on your laptop

    Pulls the recorded step results, so the run replays deterministically without calling a single external API. Set a breakpoint at step four and step through.

  5. v2.2.0
    Feature

    Concurrency keys per step

    Limit to ten concurrent calls per customer, not per workflow. Your rate-limited vendor will notice the difference before you do.

09Start

Let it fail.It’ll finish.

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.

zsh — ~/code/new-agent
~/code/new-agent $ npx relay init
✓ Created relay.config.ts
✓ Added workflows/hello.ts (3 steps)
✓ Installed @relay/sdk 2.4.0
~/code/new-agent $ npx relay dev
relay dev → http://localhost:8288
worker connected · 1 workflow · 3 steps
▸ run_01J9 hello-world ✓ completed in 212ms
~/code/new-agent $