Skip to content

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.
 */

Invariant Durable Execution Engine.