Agent Template
Copy this template into your project as agent.md, CLAUDE.md, .cursorrules, or whichever file your AI tool reads for project-wide context.
Full Template
Section titled “Full Template”# Ironflow Agent Context
This project uses Ironflow — a Continuous History platform. Everything is a recorded fact: domain events, workflow steps, authorization decisions. State is derived from history. Debugging is time-travel.
## What is Ironflow?
Ironflow is a single-binary backend platform based on the Continuous History paradigm: **record every change, replay any moment.**
- **Events (Emit):** Append-only, immutable facts. The source of truth.- **Functions (React):** Durable workflows with memoized steps. Crash-safe — resumes from the last completed step.- **Projections (Derive):** Pure reducers that build read models from events. State is always up to date.- **Time-Travel (Rewind):** Every recorded step can be replayed, inspected, and debugged. Enable with `recording: true`.
---
## Integration Workflow
When adding Ironflow to a project, follow this sequence. **Ask before creating files** — propose the structure and let the user confirm or adjust.
### Step 1: Confirm Project Structure
Before creating files, ask the user:
> "I'll set up Ironflow in this project. I see you're using [Next.js/Node.js/Go]. I suggest this structure:>> ```> [proposed structure based on framework]> ```>> Does this work, or would you prefer a different layout?"
### Step 2: Install SDK and Create Client
**TypeScript:**```bashpnpm add @ironflow/node```
Create `src/lib/ironflow.ts` (or `lib/ironflow.ts` for Next.js):```typescriptimport { createClient } from "@ironflow/node";
export const ironflow = createClient({ serverUrl: process.env.IRONFLOW_URL || "http://localhost:9123", apiKey: process.env.IRONFLOW_API_KEY,});```
**Go:**```bashgo get github.com/sahina/ironflow/sdk/go/ironflow```
Create `internal/ironflow/client.go`:```gopackage agent
import sdk "github.com/sahina/ironflow/sdk/go/ironflow"
var Client = sdk.NewClient(sdk.ClientConfig{ ServerURL: getEnv("IRONFLOW_URL", "http://localhost:9123"), APIKey: getEnv("IRONFLOW_API_KEY", ""),})```
### Step 3: Environment Variables
Add to `.env.example` (or equivalent):```IRONFLOW_URL=http://localhost:9123IRONFLOW_API_KEY= # Optional in dev mode```
### Step 4: Create Worker Entry Point
Ask the user:> "I'll create a worker entry point at `[proposed path]`. This will register your functions and projections. Should I include any initial functions, or start with an empty worker?"
### Step 5: Offer Tests
After creating functions or projections, ask:> "Should I create tests for the Ironflow components? I can add:> - Unit tests for projection handlers (pure functions, easy to test)> - Integration tests that emit events and verify function execution"
---
## Testing Ironflow Components
### Testing Projections (Unit)
```typescriptimport { describe, it, expect } from "vitest";import { orderStats } from "./order-stats";
describe("orderStats projection", () => { it("increments totalOrders on order.placed", () => { const state = orderStats.initialState(); const result = orderStats.handler(state, { name: "order.placed", data: { orderId: "123", total: 99.99 }, timestamp: new Date().toISOString(), }); expect(result.totalOrders).toBe(1); expect(result.totalRevenue).toBe(99.99); });});```
### Testing Functions (Integration)
```typescriptimport { describe, it, expect, beforeAll } from "vitest";import { createClient } from "@ironflow/node";
const ironflow = createClient();
describe("process-order function", () => { it("processes a valid order", async () => { const result = await ironflow.emit("order.placed", { orderId: "test-123", total: 49.99, });
// Poll until the triggered run completes let run = await ironflow.getRun(result.runIds[0]); while (run.status === "running" || run.status === "pending") { await new Promise((r) => setTimeout(r, 100)); run = await ironflow.getRun(result.runIds[0]); } expect(run.status).toBe("completed"); });});```
---
## Running Ironflow
```bash# Start the server (dev mode — no auth required)ironflow serve --dev
# Trigger events via CLIironflow emit order.placed --data '{"orderId": "123", "total": 99.99}'
# Query derived statecurl -s http://localhost:9123/api/v1/projections/order-stats | jq '.state.state'
# Time-travel replayironflow run listironflow inspect <run_id> --replay
# Access Dashboardopen http://localhost:9123```
---
## Project Structure Conventions
### TypeScript (Next.js)
```app/ api/ ironflow/ route.ts # Push mode handler (serve)src/ functions/ # Function definitions process-order.ts projections/ # Projection definitions order-stats.tsworker.ts # Pull mode worker entry point```
### TypeScript (Standalone Node.js)
```src/ index.ts # Worker entry point functions/ # Function definitions process-order.ts projections/ # Projection definitions order-stats.ts```
### Go
```cmd/ worker/ main.go # Worker entry pointinternal/ functions/ # Function definitions process_order.go projections/ # Projection definitions order_stats.go```
### TypeScript (DDD / CQRS)
```src/ domain/ # Domain layer (bounded contexts) order/ aggregate.ts # State rebuilders, invariants events.ts # Domain event types/schemas commands.ts # Command types/schemas customer/ aggregate.ts events.ts application/ # Application layer handlers/ # Command handlers (functions) validate-order.ts fulfill-order.ts projections/ # Read models (CQRS) order-dashboard.ts customer-orders.ts sagas/ # Process managers order-fulfillment.ts infrastructure/ ironflow.ts # Client configworker.ts # Entry point```
---
## TypeScript SDK Reference
### Installation```bashpnpm add @ironflow/node # Server-side: functions, workers, projectionspnpm add @ironflow/browser # Client-side: subscriptions, triggers, KV```
### Creating Functions
```typescriptimport { createFunction } from "@ironflow/node";
export const processOrder = createFunction( { id: "process-order", triggers: [{ event: "order.placed" }], recording: true, // Enable time-travel debugging }, async ({ event, step }) => { const data = event.data as OrderData;
// Each step is memoized — safe to retry, survives crashes const order = await step.run("validate-order", async () => { return { valid: true, orderId: data.orderId }; });
const payment = await step.run("charge-payment", async () => { return { status: "success", txId: `txn_${Date.now()}` }; });
return { orderId: order.orderId, payment }; },);```
### Function Config Options
```typescript{ id: string; // Required. Unique function identifier triggers: Trigger[]; // Required. Events that invoke this function name?: string; // Display name recording?: boolean; // Enable audit recording (time-travel debugging) recordingRetention?: string; // "7d", "30d", "90d", "forever" retry?: { // Retry configuration for failed steps maxAttempts?: number; initialDelayMs?: number; backoffFactor?: number; maxDelayMs?: number; // Max retry delay in ms (default: 300000) }; timeout?: number; // Function timeout in ms (default: 600000 = 10 min) stepTimeout?: string; // Default timeout for all step.run() calls concurrency?: { // Concurrency control limit: number; // Required. Max concurrent runs key?: string; // JSON path for grouping }; mode?: "push" | "pull"; // Execution mode secrets?: string[]; // Secret names resolved at execution time pauseBehavior?: "hold" | "release"; // Scoped injection behavior metadata?: Record<string, unknown>; description?: string; // Human-readable description shown in dashboard schema?: TEventSchema; // Zod schema for type-safe event validation debounce?: DebounceConfig; // Collapse rapid-fire events compensateOnCancel?: boolean; // Run compensations on cancel cancelOn?: CancelOnConfig[]; // Auto-cancel on matching events actorKey?: string; // JSON path for actor-based sticky routing}```
### Step Methods
```typescript// Memoized step execution — the core primitiveconst result = await step.run("step-name", async () => { return { processed: true };});
// Durable sleep — survives restartsawait step.sleep("wait-before-retry", "1h");
// Sleep until a specific timeawait step.sleepUntil("wait-for-market-open", "2026-03-16T09:30:00Z");
// Wait for an external event (human-in-the-loop, callbacks)const approval = await step.waitForEvent("wait-for-approval", { event: "order.approved", match: "data.orderId", // Correlate by field timeout: "24h",});
// Parallel branchesconst [a, b] = await step.parallel("fetch-all", [ async (s) => s.run("fetch-user", async () => fetchUser(id)), async (s) => s.run("fetch-order", async () => fetchOrder(id)),]);
// Parallel map with concurrency controlconst results = await step.map("process-items", items, async (item, s, i) => { return s.run(`process-${i}`, async () => processItem(item));}, { concurrency: 5 });
// Saga compensation (undo on failure)await step.run("charge-card", async () => charge(amount));step.compensate("charge-card", async () => refund(amount));
// Invoke another function (sync)const result = await step.invoke<InvoiceResult>("generate-invoice", { orderId });
// Invoke another function (fire-and-forget)const { runId } = await step.invokeAsync("send-report", { userId });
// Publish to a pub/sub topic (does NOT trigger functions)await step.publish("notifications", { type: "order.shipped", orderId });```
### Managed Projections
```typescriptimport { createProjection, type IronflowProjection } from "@ironflow/node";
const orderStats = createProjection({ name: "order-stats", events: ["order.placed"], initialState: () => ({ totalOrders: 0, totalRevenue: 0 }), handler: ( state: { totalOrders: number; totalRevenue: number }, event: { name: string; data: unknown }, ) => ({ totalOrders: state.totalOrders + 1, totalRevenue: state.totalRevenue + ((event.data as OrderData).total ?? 0), }),});```
### Worker Setup (Pull Mode)
```typescriptimport { createWorker, type IronflowProjection } from "@ironflow/node";
const worker = createWorker({ functions: [processOrder], projections: [orderStats as IronflowProjection],});
await worker.start();```
### Serve Setup (Push Mode — Serverless)
```typescript// app/api/ironflow/route.ts (Next.js App Router)import { serve } from "@ironflow/node";
export const POST = serve({ functions: [processOrder],});```
### Entity Streams (Event Sourcing)
```typescriptimport { createClient } from "@ironflow/node";
const client = createClient();
// Append an event to an entity streamawait client.streams.append( "order-123", { name: "order.placed", data: { total: 99.99 }, entityType: "order", // Required. Entity type for the stream }, { expectedVersion: 0, // Optimistic concurrency (in options, not input) },);
// Read the full entity historyconst events = await client.streams.read("order-123");```
### Error Handling
```typescriptimport { NonRetryableError, StepError } from "@ironflow/node";
// Non-retryable — fails immediately, no retrythrow new NonRetryableError("Invalid order ID");
// Retryable — will be retried per retry configthrow new Error("Payment gateway timeout");```
---
## Go SDK Reference
### Installation```bashgo get github.com/sahina/ironflow/sdk/go/ironflow```
### Creating Functions```goimport "github.com/sahina/ironflow/sdk/go/ironflow"
var processOrder = ironflow.CreateFunction(ironflow.FunctionConfig{ ID: "process-order", Triggers: []ironflow.Trigger{{Event: "order.placed"}}, Recording: true,}, func(ctx ironflow.Context) (any, error) { var data OrderData if err := ctx.Event.Data(&data); err != nil { return nil, err }
order, err := ironflow.Run(ctx, "validate-order", func() (Order, error) { return Order{Valid: true, OrderID: data.OrderID}, nil }) if err != nil { return nil, err }
return map[string]any{"order": order}, nil})```
### Step Methods```go// Memoized step execution (generic — infers return type)result, err := ironflow.Run(ctx, "step-name", func() (MyType, error) { return MyType{Done: true}, nil})
// With timeout overrideresult, err := ironflow.Run(ctx, "slow-step", func() (string, error) { return callExternalAPI()}, ironflow.WithTimeout(30*time.Second))
// Durable sleepironflow.Sleep(ctx, "wait-before-retry", 1*time.Hour)
// Sleep until a specific timeironflow.SleepUntil(ctx, "wait-for-open", time.Date(2026, 3, 16, 9, 30, 0, 0, time.UTC))
// Wait for an external eventevent, err := ironflow.WaitForEvent(ctx, "wait-approval", ironflow.EventFilter{ Event: "order.approved", Match: "data.orderId", Timeout: 24 * time.Hour,})
// Parallel branchesresults, err := ironflow.Parallel(ctx, "fetch-all", []func(*ironflow.BranchContext) (any, error){ func(b *ironflow.BranchContext) (any, error) { return fetchUser(id) }, func(b *ironflow.BranchContext) (any, error) { return fetchOrder(id) },})
// Parallel map with concurrencyresults, err := ironflow.Map(ctx, "process-items", items, func(item Item, b *ironflow.BranchContext, i int) (Result, error) { return processItem(item)}, ironflow.ParallelOptions{Concurrency: 5})
// Saga compensationironflow.Compensate(ctx, "charge-card", func() error { return refund(amount)})
// Invoke another function (sync)result, err := ironflow.Invoke[InvoiceResult](ctx, "generate-invoice", input)
// Invoke another function (fire-and-forget)asyncResult, err := ironflow.InvokeAsync(ctx, "send-report", input)// asyncResult.RunID contains the child run ID
// Publish to a pub/sub topicironflow.Publish(ctx, "notifications", data)```
### Managed Projections```govar orderStats = ironflow.CreateProjection(ironflow.ProjectionConfig{ Name: "order-stats", Events: []string{"order.placed"}, InitialState: func() map[string]any { // Use float64 literals so the .(float64) assertions below hold on // first in-memory call and on rebuild (JSON unmarshal yields float64). return map[string]any{"totalOrders": 0.0, "totalRevenue": 0.0} }, Handler: func(state map[string]any, event ironflow.ProjectionEvent, ctx ironflow.ProjectionContext) (map[string]any, error) { // Return a fresh map — never mutate `state` then return it. // Mutation + return of the same reference is an aliasing hazard. next := make(map[string]any, len(state)+1) for k, v := range state { next[k] = v } next["totalOrders"] = state["totalOrders"].(float64) + 1 next["totalRevenue"] = state["totalRevenue"].(float64) + event.Data["total"].(float64) return next, nil },})```
### Worker Setup (Pull Mode)```goworker := ironflow.NewWorker(ironflow.WorkerConfig{ ServerURL: "http://localhost:9123", Functions: []ironflow.Function{processOrder}, Projections: []ironflow.Projection{orderStats},})worker.Run(ctx)```
### Serve Setup (Push Mode — HTTP Handler)```gohandler := ironflow.Serve(ironflow.ServeConfig{ Functions: []ironflow.Function{processOrder},})http.ListenAndServe(":3001", handler)```
---
## Critical Patterns
### 1. All Side Effects Inside StepsAll I/O (API calls, DB writes, emails) MUST be inside `step.run()`. Code outside steps re-executes on every retry.
### 2. Unique Step IDsEach step within a function MUST have a unique ID. In loops, use `step.map()` or include the index: `step.run(\`process-\${i}\`, ...)`.
### 3. Pure ProjectionsManaged projection handlers must be **pure functions** — no API calls, no database writes, no side effects. For side effects, use external projections with `mode: "external"`.
### 4. Optimistic ConcurrencyWhen appending to entity streams, always fetch stream info first and pass `expectedVersion` to detect concurrent writes.
### 5. Enable Recording for DebuggingSet `recording: true` on any function you want to time-travel debug. This permanently records every step for replay.
### 6. Projection Type CastingWhen passing projections to `createWorker`, cast them: `projections: [myProjection as IronflowProjection]`.
## Common Mistakes to Avoid- Putting I/O outside of `step.run()` — it will re-execute on retries- Reusing step IDs in loops — use `step.map()` or indexed IDs- Forgetting `recording: true` — without it, time-travel debugging is not available- Using `initialState` as a plain object instead of a function: `initialState: () => ({...})`- Forgetting to handle timeout case from `step.waitForEvent`- Using push mode for tasks that take longer than 30 seconds — use pull mode instead- Using `import { ironflow } from "@ironflow/node"` with `ironflow.createFunction()` — prefer the direct import: `import { createFunction } from "@ironflow/node"`