Skip to content

workflows

import { startWorkflow, defineWorkflow, type WorkflowPipelineStep } from "louise-toolkit/workflows";

A thin wrapper over Cloudflare Workflows—the durable, multi-step sibling of queues. Each step’s result is persisted and its retries/backoff are owned per-step, so a flow resumes mid-way after a failure instead of replaying from the top.

function startWorkflow<P>(
workflow: Workflow<P>,
params: P,
options?: { id?: string },
): Promise<WorkflowInstance>;

Starts a new Workflow instance (the producer, mirroring enqueue). A create failure is wrapped in LouiseWorkflowError (original as cause). Pass id for idempotency—creating with an existing id throws, so a stable key coalesces a double-trigger:

await startWorkflow(
env.PUBLISH_WORKFLOW,
{ collection: "pages", id },
{ id: `publish:pages:${id}` },
);
function defineWorkflow<Env, Params, State extends object>(
steps: WorkflowPipelineStep<Env, Params, State>[],
initialState?: State,
): (env: Env, event: Readonly<WorkflowEvent<Params>>, step: WorkflowStep) => Promise<State>;
interface WorkflowPipelineStep<Env, Params, State extends object> {
name: string; // stable — Workflows keys the persisted result by it
config?: WorkflowStepConfig; // per-step retries / backoff / timeout
run: (ctx: {
env: Env;
payload: Readonly<Params>;
state: Readonly<State>;
}) => Promise<Partial<State> | void> | Partial<State> | void;
}

Turns an ordered list of named steps into a WorkflowEntrypoint.run body (mirroring processBatch). Each step runs inside step.do—durable, retried per its config—and returns a patch merged into a shared State that later steps read and the run resolves to. Because the patch is the value step.do persists, a retry/resume reuses it rather than re-running the step.

The site owns the WorkflowEntrypoint subclass and the wrangler binding (it imports cloudflare:workers), exactly as it owns the Queues queue() export—louise-toolkit/workflows stays runtime glue.

workers/site/src/workflows/publish.ts
import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from "cloudflare:workers";
import { defineWorkflow } from "louise-toolkit/workflows";
import { reindexDoc } from "louise-toolkit/content";
interface PublishParams {
collection: string;
id: number;
}
interface PublishState {
ogKey?: string;
reindexed?: boolean;
notified?: boolean;
}
const runPublish = defineWorkflow<Env, PublishParams, PublishState>([
{ name: "og", run: ({ env, payload }) => ({ ogKey: renderAndStoreOg(env, payload) }) },
{
name: "cache",
run: ({ env, state }) => {
void warmCache(env, state.ogKey);
},
},
{
name: "reindex",
config: { retries: { limit: 5, delay: "10 seconds" } },
run: async ({ env, payload }) => {
await reindexDoc(db(env.DB), pages, pagesCollection, payload.id);
return { reindexed: true };
},
},
{
name: "webhook",
run: async ({ env, payload }) => {
await fetch(env.PUBLISH_WEBHOOK, { method: "POST", body: JSON.stringify(payload) });
return { notified: true };
},
},
]);
export class PublishWorkflow extends WorkflowEntrypoint<Env, PublishParams> {
run(event: Readonly<WorkflowEvent<PublishParams>>, step: WorkflowStep) {
return runPublish(this.env, event, step);
}
}

Trigger it from the publish route with startWorkflow(env.PUBLISH_WORKFLOW, { collection, id }), and bind it in wrangler.jsonc:

"workflows": [
{ "binding": "PUBLISH_WORKFLOW", "name": "louise-publish", "class_name": "PublishWorkflow" }
]