How the homepage demo works

A Claude agent playing a chess engine, durably.

The same game you see on the homepage is a single Resonate workflow running on Cloud Run and Cloud Functions. White is a deterministic chess engine; black is Claude Haiku 4.5 picking moves one at a time. Each move is a durable step. The worker scales to zero between moves. The workflow survives it.

The live workflow.

One playGame workflow is running right now on Cloud Run. On white’s turn it calls computeMoveEngine. On black’s turn it calls agentMove, which asks Claude for a move, validates it against the legal list, and writes both the move and Claude’s reasoning to Firestore.

The board below is a Firestore subscription — no SSE, no gateway. The workflow runs forever; one game finishes, the next starts.

Live demo

Claude vs a chess engine — live.

Claude Haiku 4.5 plays black, one durable step per move.

connecting...
White · js-chess-enginedeterministic minimax · L3
Black · Claude Haiku 4.5reasoning agent · one ctx.run per move
Connecting

The call graph, live.

Each move is three durable steps: compute (engine or Claude), write, sleep. The pulsing node is what the workflow is doing right now. When it says ctx.sleep the worker is terminated — the server is holding the continuation in Postgres and will re-invoke the function when the timer fires.

Connecting to the live workflow…

Where it runs.

Five pieces, all managed GCP services. The worker scales to zero. The server is one Cloud Run container. State lives in Postgres on the workflow side and in a single Firestore doc on the presentation side — browsers subscribe to the doc and never talk to the server.

Resonate serverCloud Run · RustHolds workflow state.Cloud Function (Gen 2)chess-hero-workerScales to zero between moves.Cloud SQL · Postgresserver storageDurable across redeploys.Firestorechess/liveWorld-readable doc.BrowseronSnapshot(chess/live)No SSE. No gateway.HTTP · one step per invocationsqlx · Unix socketwriteStatesnapshot push
Resonate serverCloud Run · one container · Rust binary
Server storageCloud SQL Postgres — survives redeploys
WorkerCloud Function Gen 2 · scales to zero between moves
State bus to browsersFirestore · onSnapshot on chess/live
White playerjs-chess-engine (pure JS minimax, level 3)
Black playerClaude Haiku 4.5 · structured JSON move + one-sentence reasoning

The workflow.

The whole thing. One generator. The only Resonate primitives are ctx.run and ctx.sleep.

import type { Context } from "@resonatehq/sdk";
import { Chess } from "chess.js";
import { aiMove } from "js-chess-engine";
import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic();
const MOVE_DELAY_MS = 4500;

// White = deterministic chess engine.
async function computeMoveEngine(_ctx: Context, fen: string, level: number) {
  const move = aiMove(fen, level) as Record<string, string>;
  const [from, to] = Object.entries(move)[0];
  return `${from.toLowerCase()}${to.toLowerCase()}`;
}

// Black = Claude, given the position and the legal moves.
async function agentMove(_ctx: Context, fen: string, legal: string[], history: string) {
  const response = await anthropic.messages.parse({
    model: "claude-haiku-4-5",
    max_tokens: 512,
    system: [{ type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" } }],
    messages: [{ role: "user", content: `FEN: ${fen}\nLegal: ${legal.join(",")}\nHistory: ${history}` }],
    output_config: { format: { type: "json_schema", schema: MOVE_SCHEMA } },
  });
  // Validate, retry once on illegal move, fall back to random legal move.
  return coerceToLegal(response.parsed_output, legal);
}

export function* playGame(ctx: Context) {
  while (true) {
    const game = new Chess();
    let moveCount = 0;

    yield* ctx.run(writeState, buildState(game, undefined, moveCount));

    while (!game.isGameOver()) {
      const uci = game.turn() === "w"
        ? yield* ctx.run(computeMoveEngine, game.fen(), 3)
        : (yield* ctx.run(agentMove, game.fen(), legalMoves(game), history(game))).move;

      const move = applyUciMove(game, uci);
      moveCount++;

      yield* ctx.run(writeState, buildState(game, move, moveCount, reasoning));
      yield* ctx.sleep(MOVE_DELAY_MS);   // ← worker terminates here
    }
  }
}

What this setup proves.

  • An LLM call is just a durable step

    Claude’s move is wrapped in ctx.run — the same primitive as the engine’s move. If the API flakes, the step retries. If the response is an illegal move, the workflow corrects itself before writing state.

  • Serverless workers actually work

    Each move is a cold-friendly function invocation that ends with ctx.sleep. The container goes away between moves. The workflow keeps going.

  • The workflow outlives the server

    We redeployed the Cloud Run service mid-game. The workflow picked up from the last completed move, untouched. No intervention.

  • No streaming gateway

    Browsers read Firestore directly. There is no long-lived service between the workflow and the UI — the worker writes one doc and the clients see it.

Heads up: the server in this deployment is pre-release and unauthenticated — URL-as-secret, not for public traffic. Don’t copy this shape for anything that isn’t a demo.

Build something similar.

If you have a long-running, step-by-step process that needs to survive crashes, sleep between steps, and stay alive across redeploys — this is the shape.