Cloudflare Workflows pipeline
Cloudflare Workflows gives you durable steps on Workers via step.do. Cron schedules on the workflow binding auto-create instances — no separate cron Worker. Failed steps retry with configurable backoff.
See Running the enrich pipeline durably for the model.
Bundle setup
Section titled “Bundle setup”Your bundle runs inside the Worker (or calls out to a Node service that holds it — same bundle calls either way). For a Worker-hosted bundle, keep the database URL in secrets:
import { samesake } from "@samesake/postgres";import { llmRerank } from "@samesake/server";import { products } from "./catalog.ts";import { geminiEmbed, geminiGenerate } from "./gemini.ts";
export function createCatalogApp(env: Env) { return samesake({ url: env.SAMESAKE_DATABASE_URL, collection: products, models: { embed: geminiEmbed, generate: geminiGenerate }, rerank: llmRerank(geminiGenerate), });}Workflow class
Section titled “Workflow class”import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep,} from "cloudflare:workers";import { createCatalogApp } from "../search.ts";import { fetchChangedRows } from "../catalog-source.ts";
type Env = { SAMESAKE_DATABASE_URL: string;};
export class CatalogSync extends WorkflowEntrypoint<Env> { async run(event: WorkflowEvent<unknown>, step: WorkflowStep) { const app = createCatalogApp(this.env);
await step.do("upsert", async () => { const rows = await fetchChangedRows(); await app.enrich.upsert(rows); });
await step.do( "enrich", { retries: { limit: 5, delay: "5 seconds", backoff: "exponential" }, }, () => app.enrich.enrich(), ); }}
export class CatalogMaintenance extends WorkflowEntrypoint<Env> { async run(_event: WorkflowEvent<unknown>, step: WorkflowStep) { const app = createCatalogApp(this.env);
// Image revalidation is a caller-owned @samesake/server workflow; retry failed rows here. await step.do("retry-failed", () => app.enrich.retryFailed({ limit: 500 }), ); }}wrangler.toml — bindings and schedules
Section titled “wrangler.toml — bindings and schedules”The schedules array on each workflow binding runs the loop on a cron — Cloudflare creates a new instance automatically.
name = "samesake-pipeline"main = "src/index.ts"compatibility_date = "2026-06-09"
[[workflows]]name = "catalog-sync"binding = "CATALOG_SYNC"class_name = "CatalogSync"schedules = ["*/5 * * * *"]
[[workflows]]name = "catalog-maintenance"binding = "CATALOG_MAINTENANCE"class_name = "CatalogMaintenance"schedules = ["0 * * * *"]Webhook fast-path — create an instance from a Worker
Section titled “Webhook fast-path — create an instance from a Worker”When a product changes, create a workflow instance with that row in params, or push directly and let the scheduled sync pick it up:
import { createCatalogApp } from "./search.ts";import { CatalogSync } from "./workflows/catalog-sync.ts";
export default { async fetch(request: Request, env: Env): Promise<Response> { if (request.method === "POST" && new URL(request.url).pathname === "/webhooks/product") { const product = await request.json();
// Option A: push inline, scheduled sync handles enrich const app = createCatalogApp(env); await app.enrich.upsert([product]);
// Option B: trigger a sync workflow now const instance = await env.CATALOG_SYNC.create({ params: { trigger: "webhook" }, });
return Response.json({ ok: true, instanceId: instance.id }); }
return new Response("Not found", { status: 404 }); },};Use step.sleep("name", "20 seconds") between steps if you need deliberate delays — instances in a waiting state don’t count toward concurrency limits.
Why this fits samesake
Section titled “Why this fits samesake”Workers + Workflows keep everything at the edge if your app already lives there. samesake’s incrementality means each scheduled instance still runs two bundle calls over the whole collection — cheap when only dozens of rows changed. You don’t fan out per product; Cloudflare’s durability covers per-step retries.
Where to go next
Section titled “Where to go next”- Concept and platform picker — Running the enrich pipeline durably
- Row statuses — Pipeline lifecycle
- Cloudflare Workflows docs — developers.cloudflare.com/workflows
Run it end to end
Section titled “Run it end to end”The runnable, tested version of this guide lives in examples/pipeline-cloudflare-workflows — deterministic stubs, no API key. Run bun test (Postgres-backed examples need SAMESAKE_DATABASE_URL).