Workflow Examples
Explore complete, real-world workflow patterns built with Invariant.
1. Simple Deterministic Workflow
A pure data processing pipeline without LLMs or side-effects.
ts
import { app } from "./runtime";
import { z } from "zod";
export const dataPipeline = app.workflow("data-pipeline")
.step("clean-input", async ({ input }) => {
return {
cleanQuery: input.query.trim().toLowerCase(),
};
})
.step("compute-stats", async ({ state }) => {
return {
wordCount: state.cleanQuery.split(/\s+/).length,
timestamp: Date.now(),
};
});2. AI Reasoning Workflow (.reason())
Extracting structured intent from customer support tickets using Gemini.
ts
import { app } from "./runtime";
import { z } from "zod";
const TicketIntentSchema = z.object({
category: z.enum(["BILLING", "TECHNICAL", "ACCOUNT_ACCESS", "OTHER"]),
urgency: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]),
summary: z.string(),
});
export const ticketClassifier = app.workflow("ticket-classifier")
.reason("classify-intent", {
prompt: ({ input }) => `
Analyze customer message: "${input.message}"
Classify the category, urgency level, and provide a 1-sentence summary.
`,
schema: TicketIntentSchema,
temperature: 0.1,
});3. Side-Effects & Outbox Workflow (.capability())
Charging a customer card via Stripe with explicit idempotency key guarantees.
ts
import { app } from "./runtime";
import { stripe } from "./services/stripe";
export const paymentWorkflow = app.workflow("payment-processing")
.capability("charge-card", {
idempotencyKey: ({ execution }) => `charge:${execution.id}`,
handler: async ({ input }) => {
const charge = await stripe.charges.create({
amount: input.amount,
currency: "usd",
customer: input.customerId,
}, {
idempotencyKey: `charge:${execution.id}`, // Stripe API idempotency
});
return {
chargeId: charge.id,
status: charge.status,
};
},
});4. Human-in-the-Loop & Wait Workflow (.wait())
Suspending workflow execution for human approval, with an automatic 48-hour timeout fallback.
ts
import { app } from "./runtime";
import { z } from "zod";
export const highValueApproval = app.workflow("high-value-approval")
.capability("notify-manager", {
idempotencyKey: ({ execution }) => `notify:${execution.id}`,
handler: async ({ input }) => {
await slack.postMessage({
channel: "#manager-approvals",
text: `Approval requested for $${input.amount}. Run ID: ${execution.id}`,
});
return { notified: true };
},
})
.wait("wait-for-human-decision", {
timeout: "48h",
})
.step("process-decision", async ({ state, incomingEvent }) => {
if (incomingEvent.type === "TIMER_FIRED") {
return { status: "REJECTED", reason: "Approval timed out after 48h" };
}
return { status: incomingEvent.payload.approved ? "APPROVED" : "REJECTED" };
});5. Process Crash & Crash Recovery Simulation
What happens when your Node.js process crashes mid-execution?
ts
import { app } from "./runtime";
export const resilientRun = app.workflow("resilient-run")
.step("step-1", async () => ({ progress: 1 }))
.capability("risky-execution", {
idempotencyKey: ({ execution }) => `risky:${execution.id}`,
handler: async ({ state }) => {
console.log("💥 SIMULATING SEVERE SERVER CRASH (process.exit)...");
process.exit(1); // Server crashes mid-capability!
},
});
/*
* WHAT HAPPENS WHEN YOU RESTART THE SERVER:
*
* 1. Server restarts and runs app.start().
* 2. Reclaimer Worker finds expired lease for 'resilient-run'.
* 3. Reclaimer re-enqueues the run.
* 4. Step 1 (progress: 1) is NOT re-executed (it was already committed).
* 5. Capability 'risky-execution' is safely re-tried using its idempotency key.
*/