Quickstart

Install and configure the Workflows fragment.

Overview

The Workflows fragment provides durable, replayable workflow execution with steps, timers, retries, and external events. This quickstart walks through defining a workflow, wiring durable hooks, and exposing routes.

Installation

Install the fragment and database packages:

npm install @fragno-dev/workflows @fragno-dev/db

For the test harness example, also install the Fragno test package:

npm install --save-dev @fragno-dev/test

Install the Workflows Agent Skill:

npx skills add https://github.com/rejot-dev/fragno --skill fragno-workflows

Then ask: "Use fragno-workflows to define and test a replay-safe workflow."

Define a Workflow

Create a workflow definition and a registry of workflows:

lib/workflows.ts
import {
  defineWorkflow,
  type WorkflowEvent,
  type WorkflowStep,
} from "@fragno-dev/workflows/workflow";

type ApprovalParams = {
  requestId: string;
  amount: number;
};

type ApprovalEvent = { approved: boolean };

type FulfillmentEvent = { confirmationId: string };

export const ApprovalWorkflow = defineWorkflow(
  { name: "approval-workflow" },
  async (event: WorkflowEvent<ApprovalParams>, step: WorkflowStep) => {
    const approval = await step.waitForEvent<ApprovalEvent>("approval", {
      type: "approval",
      timeout: "15 min",
    });

    await step.sleep("cooldown", "2 s");

    const fulfillment = await step.waitForEvent<FulfillmentEvent>("fulfillment", {
      type: "fulfillment",
      timeout: "15 min",
    });

    return { request: event.payload, approval, fulfillment };
  },
);

export const workflows = {
  approval: ApprovalWorkflow,
} as const;

Optional: pass a Standard Schema as schema to validate params, and an outputSchema to type output end-to-end.

import { z } from "zod";

const paramsSchema = z.object({ requestId: z.string(), amount: z.number() });
const outputSchema = z.object({ confirmationId: z.string() });

export const ApprovalWorkflow = defineWorkflow(
  { name: "approval-workflow", schema: paramsSchema, outputSchema },
  async (event, step) => ({ confirmationId: "conf_123" }),
);

Create the Fragment Server

Wire the durable hooks dispatcher and fragment definition:

lib/workflows-fragment.ts
import { defaultFragnoRuntime } from "@fragno-dev/core";
import { type DatabaseAdapter } from "@fragno-dev/db";
import { createDurableHooksProcessor } from "@fragno-dev/db/dispatchers/node";
import { createWorkflowsFragment } from "@fragno-dev/workflows";
import { workflows } from "./workflows";

export function createWorkflowsFragmentServer(adapter: DatabaseAdapter<any>) {
  const fragment = createWorkflowsFragment(
    {
      workflows,
      runtime: defaultFragnoRuntime,
    },
    { databaseAdapter: adapter },
  );

  const dispatcher = createDurableHooksProcessor([fragment], {
    pollIntervalMs: 2000,
  });

  dispatcher.startPolling();
  process.on("SIGTERM", () => dispatcher.stopPolling());

  return { fragment, dispatcher };
}

The in-process dispatcher is ideal for local dev. For Cloudflare deployments use @fragno-dev/db/dispatchers/cloudflare-do.

Mount the Routes

Mount the fragment using your framework adapter. See Integrating a Fragment for framework-specific examples.

Run Database Migrations

Generate SQL migrations and apply them with the Fragno CLI:

npx fragno-cli db generate lib/workflows-fragment.ts
npx fragno-cli db migrate lib/workflows-fragment.ts

For Drizzle or Prisma, generate the schema output and apply it with that ORM's tooling instead.

Test Workflows

Use the test harness to drive workflow ticks with a controllable clock and deterministic runtime:

lib/workflows.test.ts
import { buildDatabaseFragmentsTest } from "@fragno-dev/test";
import { createWorkflowsTestHarness, createWorkflowsTestRuntime } from "@fragno-dev/workflows/test";
import { workflows } from "./workflows";

const runtime = createWorkflowsTestRuntime({ startAt: 0, seed: 123 });
const harness = await createWorkflowsTestHarness({
  workflows,
  adapter: { type: "in-memory" },
  testBuilder: buildDatabaseFragmentsTest(),
  runtime,
  autoTickHooks: false,
});

const instanceId = await harness.createInstance("approval", {
  params: { requestId: "req_1", amount: 125 },
});

await harness.runUntilIdle({
  workflowName: "approval-workflow",
  instanceId,
  reason: "create",
});
await harness.sendEvent("approval", instanceId, {
  type: "approval",
  payload: { approved: true },
});
await harness.runUntilIdle({
  workflowName: "approval-workflow",
  instanceId,
  reason: "event",
});

harness.clock.advanceBy("2 s");
await harness.runUntilIdle({
  workflowName: "approval-workflow",
  instanceId,
  reason: "wake",
});

await harness.test.cleanup();

For focused harness tests and end-to-end scenarios, see Workflow Testing.

Drive the Runner

For external schedulers, run a durable hooks dispatcher (Node or Cloudflare DO) so hooks are processed when work is enqueued.

Next Steps

  • Review the replay and idempotency guidance in Rules of Workflows.
  • Publish progress and receive active-step events with Step Events & Emissions.
  • Review workflow steps, events, and currently persisted emissions using the /history route.