Quick Start
Build and execute your first durable workflow in less than 5 minutes.
1. Install
Install Invariant with embedded local SQLite storage (no Docker or external database required):
bash
npm install @invariant-tech/sdk@beta @invariant-tech/sqlite@beta zod2. Define and Run Your First Workflow
Create src/index.ts to initialize Invariant with embedded storage, define a durable order-processing workflow, and execute it inside a durable session:
ts
import { invariant } from "@invariant-tech/sdk";
import { sqlite } from "@invariant-tech/sqlite";
import { z } from "zod";
export const app = invariant({
storage: sqlite("./data/invariant.db"),
});
export const orderWorkflow = app
.workflow("order-processing", {
inputSchema: z.object({
orderId: z.string(),
amount: z.number(),
userTier: z.enum(["STANDARD", "VIP"]),
}),
})
.step("calculate-discount", ({ input }) => ({
discount: input.userTier === "VIP" ? input.amount * 0.2 : 0,
finalTotal: input.userTier === "VIP" ? input.amount * 0.8 : input.amount,
}))
.capability("charge-customer", {
idempotencyKey: "charge:{{runId}}",
handler: async ({ state }) => {
console.log(`💳 Charging $${state.finalTotal} for order ${state.orderId}`);
return {
paymentId: `pay_${Date.now()}`,
status: "COMPLETED",
};
},
});
async function main() {
const session = await app.sessions.loadOrCreateSession("sess_quickstart", "usr_101");
const run = await session.startWorkflow(orderWorkflow, {
orderId: "ord_9981",
amount: 150,
userTier: "VIP",
});
console.log(`🚀 Workflow started: ${run.runId}`);
console.log("Execution Result:", session.snapshot().projectedState);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});3. Run the Workflow
Execute the file directly with npx tsx:
bash
npx tsx src/index.tsOutput:
text
💳 Charging $120 for order ord_9981
🚀 Workflow started: run_order-processing_1787141932160
Execution Result: {
orderId: 'ord_9981',
amount: 150,
userTier: 'VIP',
discount: 30,
finalTotal: 120,
paymentId: 'pay_1787141932185',
status: 'COMPLETED'
}Embedded Durability from Day One
No database servers. No Redis. No Docker setup. SQLite stores execution history, events, session context, and outbox commands locally with WAL journaling.
4. Add Probabilistic AI Reasoning (.reason())
Now introduce LLM reasoning into the workflow by installing @invariant-tech/anthropic (or @invariant-tech/openai / @invariant-tech/google):
bash
npm install @invariant-tech/anthropic@betaExport your API key:
bash
export ANTHROPIC_API_KEY=your_anthropic_api_keyAdd the model provider to your runtime and attach a .reason() step:
ts
import { invariant } from "@invariant-tech/sdk";
import { sqlite } from "@invariant-tech/sqlite";
import { anthropic, ANTHROPIC_MODELS } from "@invariant-tech/anthropic";
import { z } from "zod";
type TicketClassification = {
category: "BILLING" | "TECHNICAL" | "ACCOUNT" | "OTHER";
sentiment: "POSITIVE" | "NEUTRAL" | "NEGATIVE";
};
export const app = invariant({
storage: sqlite("./data/invariant.db"),
models: {
default: anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
defaultModel: ANTHROPIC_MODELS.CLAUDE_SONNET_5,
}),
},
});
export const supportWorkflow = app
.workflow("support-ticket", {
inputSchema: z.object({
message: z.string(),
userTier: z.enum(["STANDARD", "VIP"]),
}),
})
.reason<TicketClassification>("classify-ticket", {
instruction: "Classify the support message in execution context by category and sentiment.",
schema: {
type: "object",
properties: {
category: { type: "string", enum: ["BILLING", "TECHNICAL", "ACCOUNT", "OTHER"] },
sentiment: { type: "string", enum: ["POSITIVE", "NEUTRAL", "NEGATIVE"] },
},
required: ["category", "sentiment"],
additionalProperties: false,
},
})
.step("set-priority", ({ state, input }) => ({
priority: input.userTier === "VIP" || state.sentiment === "NEGATIVE" ? "HIGH" : "NORMAL",
}))
.capability("create-ticket", {
idempotencyKey: "support-ticket:{{runId}}",
handler: async ({ state }) => {
console.log(`🎫 Creating ticket [${state.priority}] (${state.category})`);
return {
ticketId: `tkt_${Date.now()}`,
status: "OPEN",
assignedTeam: state.category === "BILLING" ? "Finance" : "Support",
};
},
});
async function main() {
const session = await app.sessions.loadOrCreateSession("sess_support_1", "usr_202");
await session.startWorkflow(supportWorkflow, {
message: "I was double charged on my subscription and need an urgent refund!",
userTier: "VIP",
});
console.log("Support Ticket Result:", session.snapshot().projectedState);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});The Execution Model in Action
text
User message
│
reason() ──► classify-ticket (AI interprets sentiment & category)
│
step() ──► set-priority (Code calculates priority based on VIP tier & sentiment)
│
capability() ──► create-ticket (External system creates ticket with idempotency).reason()delegates a bounded semantic question to the LLM..step()executes deterministic business logic without model involvement..capability()executes external side-effects backed by the Transactional Outbox.- The configured store persists committed execution state and event history. A Session coordinates the current run and can be restored at a registered
.wait()boundary; it is not an arbitrary-runIdcommand-recovery handle.
Switching Durable Storage to PostgreSQL
You can swap the embedded SQLite store for PostgreSQL without changing workflow definitions or schemas. This changes persistence and coordination primitives; it does not add the missing public command-claim, execution-attach, or background recovery host APIs. See the Recovery Contract Matrix.
bash
npm install @invariant-tech/postgres@betats
import { invariant } from "@invariant-tech/sdk";
import { postgres } from "@invariant-tech/postgres";
export const app = invariant({
storage: postgres({
connectionString: process.env.DATABASE_URL!,
}),
});Next Steps
- Mental Model — Understand how Workflows, Agents, and Executions fit together.
- Agents & Actions — Learn how an
app.agent()evaluates user intent to drive workflows automatically. - Crash Recovery Primitives — Understand what survives a crash and which public orchestration APIs are still missing.