Skip to content

Workflow Patterns Catalog

These patterns use the current Beta API. External service variables are application-owned dependencies; the workflow signatures themselves are copyable.

Shared Setup

ts
import { invariant } from "@invariant-tech/sdk";
import { sqlite } from "@invariant-tech/sqlite";

const deterministicModel = {
  provider: "fixture",
  defaultModel: "patterns-fixture-v1",
  async generateReasoning() {
    return {
      result: { category: "BILLING", urgency: "HIGH" },
    };
  },
};

export const app = invariant({
  storage: sqlite("./data/invariant.db"),
  models: { default: deterministicModel },
});

The fixture makes bounded .reason() examples executable without credentials or network access. Replace it with a provider adapter from Model Adapters in an application.

Deterministic Pipeline

ts
import { z } from "zod";
import { app } from "./shared-setup.js";

export const dataPipeline = app
  .workflow("data-pipeline", {
    inputSchema: z.object({ query: z.string() }),
  })
  .step("clean-input", ({ input }) => ({
    cleanQuery: input.query.trim().toLowerCase(),
  }))
  .step("compute-stats", ({ state }) => ({
    wordCount: state.cleanQuery.split(/\s+/).length,
  }));

Keep .step() deterministic and free of external I/O.

Bounded Model Reasoning

ts
import { z } from "zod";
import { app } from "./shared-setup.js";

type TicketClassification = {
  category: "BILLING" | "TECHNICAL" | "OTHER";
  urgency: "LOW" | "MEDIUM" | "HIGH";
};

export const ticketClassifier = app
  .workflow("ticket-classifier", {
    inputSchema: z.object({ message: z.string() }),
  })
  .reason<TicketClassification>("classify-intent", {
    instruction: "Classify the support message in execution context.",
    schema: {
      type: "object",
      properties: {
        category: { type: "string", enum: ["BILLING", "TECHNICAL", "OTHER"] },
        urgency: { type: "string", enum: ["LOW", "MEDIUM", "HIGH"] },
      },
      required: ["category", "urgency"],
      additionalProperties: false,
    },
  });

A .reason() output becomes state only after schema validation. A declared fallback is a deterministic graph edge for a typed reasoning failure; it is not a retry policy.

External Capability with Stable Identity

ts
import { z } from "zod";
import { app } from "./shared-setup.js";

const payments = {
  charge: async (request: {
    amount: number;
    customerId: string;
    idempotencyKey: string;
  }) => ({ id: `charge_${request.idempotencyKey}`, status: "captured" }),
};

export const paymentWorkflow = app
  .workflow("payment-processing", {
    inputSchema: z.object({
      amount: z.number(),
      customerId: z.string(),
      paymentRequestId: z.string(),
    }),
  })
  .capability("charge-card", {
    idempotencyKey: "charge:{{runId}}",
    handler: async ({ input }) => {
      const charge = await payments.charge({
        amount: input.amount,
        customerId: input.customerId,
        idempotencyKey: input.paymentRequestId,
      });
      return { chargeId: charge.id, chargeStatus: charge.status };
    },
  });

Invariant persists command intent using the resolved charge: identity. Because the current handler contract does not expose that resolved runtime key, the application also supplies a stable paymentRequestId to the provider for provider-side deduplication.

Human Approval Wait

ts
import { z } from "zod";
import { app } from "./shared-setup.js";

type ManagerDecision = {
  approved: boolean;
  approverId: string;
};

export const approvalWorkflow = app
  .workflow("high-value-approval", {
    inputSchema: z.object({ requestId: z.string() }),
  })
  .wait<ManagerDecision>("manager-decision", {
    schema: z.object({
      approved: z.boolean(),
      approverId: z.string(),
    }),
    presentation: {
      prompt: "Approve this request?",
      choices: ["approve", "reject"],
    },
  })
  .step("record-decision", ({ state }) => ({
    finalStatus: state.approved ? "APPROVED" : "REJECTED",
  }));

The Beta host does not schedule automatic wait timeouts. Application scheduling code can cancel the workflow or submit a typed escalation decision.

Deterministic Branch

ts
import { z } from "zod";
import { app } from "./shared-setup.js";

export const refundRouter = app
  .workflow("refund-router", {
    inputSchema: z.object({ amount: z.number() }),
  })
  .step("evaluate-risk", ({ input }) => ({
    risk: input.amount > 1_000 ? "HIGH" as const : "LOW" as const,
  }))
  .branch("risk-route", ({ state }) => state.risk, {
    LOW: app.fragment("auto-refund").step("approve", () => ({ approved: true })),
    HIGH: app.fragment("manual-refund").wait<{ approved: boolean }>("review", {
      schema: z.object({ approved: z.boolean() }),
    }),
  });

Bounded Repeat

ts
import { app } from "./shared-setup.js";

async function checkExternalStatus() {
  return { externalStatus: "PENDING" };
}

const pollBody = app.fragment("poll-body")
  .capability("check-status", checkExternalStatus);

export const pollingWorkflow = app.workflow("bounded-poll")
  .repeat("poll", {
    maxIterations: 5,
    do: pollBody,
  });

repeat expresses business iteration. Configurable automatic infrastructure retries are not part of the current Beta.

Cancellation and Compensation

ts
import { z } from "zod";
import { app } from "./shared-setup.js";

async function releaseReservedSlot() {
  return { slotReleased: true };
}

async function reserveSlot() {
  return { slotId: "slot_123" };
}

const cancelBooking = app.fragment("cancel-booking")
  .capability("release-slot", {
    idempotencyKey: "release-slot:{{runId}}",
    handler: releaseReservedSlot,
  });

export const bookingWorkflow = app.workflow("booking", {
  lifecycle: { cancel: cancelBooking },
})
  .capability("reserve-slot", reserveSlot)
  .wait<{ confirmed: boolean }>("confirm-booking", {
    schema: z.object({ confirmed: z.boolean() }),
  });

Application code cancels with await session.cancelWorkflow(reason). Compensation appends new facts; it never erases the original effect.

Experimental Showcases

The larger Boulevard, Computer Use, and Tarot Live applications pass their current package typechecks and mock-backed tests. They remain architecture tours until their live external integrations have acceptance coverage.

Invariant Durable Execution Engine.