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.