Vercel Workflows pipeline
Vercel Workflows (Workflow DevKit, package workflow) marks orchestrators with "use workflow" and durable steps with "use step". Unhandled errors retry automatically; FatalError skips retries. Recurring syncs use a Vercel Cron Job that hits an API route calling start() from workflow/api.
See Running the enrich pipeline durably for the model.
Bundle setup
Section titled “Bundle setup”import { samesake } from "@samesake/postgres";import { llmRerank } from "@samesake/server";import { products } from "../catalog.ts";import { geminiEmbed, geminiGenerate } from "../gemini.ts";
export const app = samesake({ url: process.env.SAMESAKE_DATABASE_URL!, collection: products, models: { embed: geminiEmbed, generate: geminiGenerate }, rerank: llmRerank(geminiGenerate),});Workflow and steps
Section titled “Workflow and steps”Each bundle call lives in its own step function — failures retry per step, not per workflow.
import { FatalError } from "workflow";import { app } from "../lib/search.ts";import { fetchChangedRows } from "../catalog-source.ts";
export async function syncCatalog() { "use workflow";
await upsertStep(); await enrichStep();}
async function upsertStep() { "use step";
if (!process.env.SAMESAKE_DATABASE_URL) { throw new FatalError("SAMESAKE_DATABASE_URL is not set"); }
const rows = await fetchChangedRows(); await app.enrich.upsert(rows);}
async function enrichStep() { "use step"; await app.enrich.enrich();}import { app } from "../lib/search.ts";
export async function catalogMaintenance() { "use workflow";
await retryFailedStep();}
// Image revalidation is a caller-owned @samesake/server workflow; retry failed rows here.async function retryFailedStep() { "use step"; await app.enrich.retryFailed({ limit: 500 });}Use import { sleep } from "workflow" and await sleep("5s") when you need a delay between steps — sleeping doesn’t consume compute.
Start workflows from API routes
Section titled “Start workflows from API routes”Trigger runs with start() from workflow/api. It returns immediately; the workflow executes asynchronously.
import { start } from "workflow/api";import { syncCatalog } from "@/workflows/sync-catalog";
export async function GET() { const run = await start(syncCatalog, []); return Response.json({ message: "sync started", runId: run.runId });}import { start } from "workflow/api";import { catalogMaintenance } from "@/workflows/maintenance";
export async function GET() { const run = await start(catalogMaintenance, []); return Response.json({ message: "maintenance started", runId: run.runId });}Vercel Cron Jobs
Section titled “Vercel Cron Jobs”Point crons at those routes in vercel.json:
{ "crons": [ { "path": "/api/cron/sync-catalog", "schedule": "*/5 * * * *" }, { "path": "/api/cron/maintenance", "schedule": "0 * * * *" } ]}Secure cron routes with CRON_SECRET (Vercel sends it as Authorization: Bearer …) so random HTTP clients can’t trigger full syncs.
Webhook fast-path
Section titled “Webhook fast-path”import { start } from "workflow/api";import { app } from "@/lib/search";import { syncCatalog } from "@/workflows/sync-catalog";
export async function POST(request: Request) { const product = await request.json();
await app.enrich.upsert([product]);
// Next cron picks up enrich — or start the workflow now: const run = await start(syncCatalog, []);
return Response.json({ ok: true, runId: run.runId });}Why this fits samesake
Section titled “Why this fits samesake”Vercel Workflows keep orchestration in your repo — no separate queue service if you’re already on Vercel. Each "use step" boundary matches a bundle call: a transient embed failure retries enrichStep without re-running upsertStep. samesake’s idempotent incrementality still means unchanged rows are cheap inside each step.
Where to go next
Section titled “Where to go next”- Concept and platform picker — Running the enrich pipeline durably
- Row statuses — Pipeline lifecycle
- Workflow DevKit docs — useworkflow.dev
Run it end to end
Section titled “Run it end to end”The runnable, tested version of this guide lives in examples/pipeline-vercel-workflows — deterministic stubs, no API key. Run bun test (Postgres-backed examples need SAMESAKE_DATABASE_URL).