Skip to content

Quickstart

Get started with Invariant in under 5 minutes. In this guide, you will install the framework, initialize a PostgreSQL runtime, define a durable AI workflow, and execute it.

1. Installation

Install the core Invariant packages into your TypeScript project:

bash
npm install @invariant/runtime @invariant/postgres @invariant/sdk

Ensure you have a running PostgreSQL database (v14+ recommended) and a TypeScript configuration (tsconfig.json) targeting ES2022 or higher.

2. Initialize the Invariant Runtime

Create an instance of Invariant backed by @invariant/postgres:

ts
import { invariant } from "@invariant/sdk";
import { postgres } from "@invariant/postgres";
import pg from "pg";

// 1. Setup PostgreSQL pool
const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL || "postgres://postgres:postgres@localhost:5432/invariant",
});

// 2. Initialize Invariant Runtime
export const app = invariant({
  store: postgres({ pool }),
});

3. Define Your First Workflow

Use the Fluent SDK (@invariant/sdk) to build a durable workflow. Each step explicitly declares its computation boundary:

ts
import { z } from "zod";
import { app } from "./runtime";

// Schema definitions
const UserInputSchema = z.object({
  userId: z.string(),
  refundReason: z.string(),
});

const PolicyDecisionSchema = z.object({
  approved: z.boolean(),
  explanation: z.string(),
  refundAmount: z.number(),
});

export const refundWorkflow = app.workflow("customer-refund")
  // 1. Deterministic Step (0 I/O)
  .step("load-customer-data", async ({ input }) => {
    return {
      customerId: input.userId,
      accountTier: "VIP",
    };
  })

  // 2. Probabilistic AI Reasoning Boundary
  .reason("evaluate-refund-policy", {
    prompt: ({ state }) => `
      Customer Tier: ${state.accountTier}
      Refund Reason: ${state.refundReason}
      
      Evaluate whether this customer is eligible for a full refund based on VIP policies.
    `,
    schema: PolicyDecisionSchema,
  })

  // 3. Durable Capability (Side Effect with Idempotency Key)
  .capability("issue-stripe-refund", {
    idempotencyKey: ({ execution }) => `stripe-refund:${execution.id}`,
    handler: async ({ state }) => {
      if (!state.approved) {
        return { status: "SKIPPED", reason: "Policy denied" };
      }
      
      // Simulate external API call
      return {
        status: "SUCCESS",
        transactionId: `ch_${Date.now()}`,
        amount: state.refundAmount,
      };
    },
  })

  // 4. Durable Wait Boundary (Suspends until event received)
  .wait("wait-for-customer-feedback", {
    timeout: "24h",
  });

4. Register and Start the Runtime Worker

Start the runtime worker to process queued executions, handle Outbox side-effects, and reclaim crashed runs:

ts
import { app } from "./runtime";
import { refundWorkflow } from "./workflows/refund";

async function main() {
  // Register workflow
  app.register(refundWorkflow);

  // Start background worker loop (Fast-path + Reclaimer)
  await app.start();

  console.log("🚀 Invariant Runtime active and listening for events.");
}

main().catch(console.error);

5. Dispatch an Event

Dispatch a RuntimeEvent to trigger your workflow:

ts
const runId = await app.dispatch({
  workflow: "customer-refund",
  event: {
    type: "RUN_CREATED",
    payload: {
      userId: "usr_9981",
      refundReason: "Item arrived damaged during shipping.",
    },
  },
});

console.log(`Started workflow run: ${runId}`);

What Happens Behind the Scenes?

  1. Transactional Ingestion: The RUN_CREATED event is persisted to PostgreSQL inside workflow_events with sequence seq = 1.
  2. Deterministic Reduction: ExecutionEngine.transition() computes the initial state and routes to load-customer-data.
  3. Reasoning Boundary: The engine emits a ReasonCommand. The runtime executes the LLM prompt and feeds the structured JSON back as a CAPABILITY_COMPLETED RuntimeEvent.
  4. Atomic Outbox Commit: The decision triggers issue-stripe-refund. State, event log, and the Outbox command commit in a single FOR UPDATE PostgreSQL transaction.
  5. Suspension: The workflow reaches .wait() and safely suspends with status = 'suspended'. Zero CPU/memory overhead while waiting.

Next Steps

Invariant Durable Execution Engine.