Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

TanStack Workflows on Restate

Run your TanStack workflows on Restate.

npm install @restatedev/tanstack-workflows @restatedev/restate-sdk @tanstack/workflow-core

Or start from the ready-to-run template (server + client):

npx degit restatedev/tanstack-workflows/template my-workflows

1. Write a workflow

A plain TanStack workflow:

// checkout.ts
import { createWorkflow } from "@tanstack/workflow-core";
import { z } from "zod";

export const checkout = createWorkflow({
  id: "checkout",
  input: z.object({ userId: z.string(), amount: z.number() }),
  output: z.object({ status: z.enum(["approved", "rejected"]) }),
}).handler(async (ctx) => {
  const charge = await ctx.step("charge-card", (step) =>
    stripe.charges.create(
      { customer: ctx.input.userId, amount: ctx.input.amount },
      { idempotencyKey: step.id }
    )
  );

  if (ctx.input.amount > 10_000) {
    const decision = await ctx.approve({
      id: "manager",
      title: "Approve large charge?",
    });
    if (!decision.approved) return { status: "rejected" as const };
  }

  await ctx.step("send-receipt", () => sendReceipt(charge.id));
  return { status: "approved" as const };
});

2. Serve it with Restate

// server.ts
import * as restate from "@restatedev/restate-sdk";
import { restateWorkflows } from "@restatedev/tanstack-workflows";
import { checkout } from "./checkout.js";

await restate.serve({ services: restateWorkflows(checkout), port: 9080 });

Start Restate:

docker run --name restate --network=host ghcr.io/restatedev/restate:main

And register your service:

npx @restatedev/restate deployments register http://localhost:9080

3. Start runs, send approvals and signals

npm install @restatedev/tanstack-workflows-client @restatedev/restate-sdk-clients
import { createRestateWorkflowRuntime } from "@restatedev/tanstack-workflows-client";

const runtime = createRestateWorkflowRuntime({ url: "http://localhost:8080" });

// Start a run. `runId` is the idempotency key: starting it twice is a no-op.
await runtime.startRun({
  workflowId: "checkout",
  runId: "order-42",
  input: { userId: "cus_123", amount: 20_000 },
});

// Later, from anywhere (a webhook, an admin UI, ...):
await runtime.deliverApproval({
  workflowId: "checkout",
  runId: "order-42",
  approval: { approvalId: "manager", approved: true },
});

// Wait for the result...
const output = await runtime.attachRun("checkout", "order-42");
// ...or just check on it.
const run = await runtime.getRun("checkout", "order-42");
// { runId, workflowId, status: "queued" | "running" | "finished" | "errored", output?, error? }

Signals work the same way, resolving ctx.waitForEvent(name):

await runtime.deliverSignal({
  workflowId: "checkout",
  runId: "order-42",
  signalId: "evt-1",
  name: "payment-confirmed",
  payload: { paymentId: "pay_1" },
});

How it maps

TanStack ctx On Restate
step(id, fn, { retry }) ctx.run: journaled, retried, never re-executed on replay
sleep / sleepUntil Durable Restate timer
waitForEvent(name) Restate signal; early deliveries are buffered until the run waits
approve({ id, ... }) Restate signal, resolved by deliverApproval with that id
now() / uuid() Deterministic ctx.date.now() / ctx.rand.uuidv4()
runId Idempotency key of the run

You get Restate's UI, tracing and journal introspection for free.

The Restate services

Each workflow becomes one Restate service named after its id, with three handlers:

Handler What
run The entry point: input = the workflow's input, output = its output. Needs an idempotency key: the runId.
signal { invocationId, name, payload? }: resolves a waitForEvent(name) of the run (deliverSignal).
approve { invocationId, approvalId, approved, feedback? }: resolves an approve({ id }) of the run (deliverApproval).

invocationId is the run invocation's id, from the ingress lookup API; the client does that for you.

Each workflow service carries this service metadata, so registries and UIs can recognize it:

Key Value
restate.dev/integration tanstack-workflow
restate.dev/integration-version the @restatedev/tanstack-workflows version

And run's input and output carry the workflow's input / output schemas as JSON Schema in the discovery manifest, when the schema library can produce one (Standard JSON Schema, e.g. zod 4). Otherwise they're left out. Validation is unchanged: the workflow validates its input and output itself.

To deliver from another Restate handler without going through signal / approve, resolve the run's signals directly: event:<name> with the event's payload, approval:<id> with { approved, feedback? }.

ctx.invocation(invocationId).signal("approval:manager").resolve({ approved: true });

Contributing

See DEVELOPMENT.md.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages