- AI & Agents
- Build with AI Assistants
Build with AI Assistants
Use your favorite AI coding assistant to build a complete Continuous History system — events, durable functions, projections, and time-travel debugging — guided by AI-generated code.
Prerequisites
Section titled “Prerequisites”- Ironflow running locally (
ironflow serve --dev) - An AI coding assistant (Claude Code, Cursor, Copilot, Windsurf, etc.)
- Node.js 22+ and pnpm (or Go 1.26+ if following the Go track, or Python 3.10+ if following the Python track)
Step 1: Give Your Agent Ironflow Context
Section titled “Step 1: Give Your Agent Ironflow Context”The fastest route is one sentence. Paste it into your coding agent:
Read https://ironflow.run/start.md and set up Ironflow in this project.The page tells the agent to install the engine if it is missing, install the skills, and show you each command first. To install only the skills, or to use ironflow skills sync yourself, see AI Skills.
If your tool does not support skills, copy the agent template from the Agent Template page and save it in your project:
your-project/├── agent.md # AI context file├── src/│ └── workflows/└── package.jsonDifferent AI tools read different files:
| Tool | Recommended File |
|---|---|
| Claude Code | CLAUDE.md or agent.md |
| Cursor | .cursor/rules/*.mdc (preferred) or agent.md |
| GitHub Copilot | .github/copilot-instructions.md or agent.md |
| Windsurf | .windsurfrules or agent.md |
| Other | agent.md (most tools read markdown) |
The MCP server (ironflow mcp) is a deeper integration path — see AI MCP Server.
Step 2: Start Ironflow
Section titled “Step 2: Start Ironflow”ironflow serve --devThe --dev flag disables authentication so you can start building immediately. Server runs at http://localhost:9123.
Step 3: Ask the AI to Build a Recorded System
Section titled “Step 3: Ask the AI to Build a Recorded System”Use this prompt with your AI assistant. Notice it covers all four Continuous History pillars — emit, react, derive, and rewind:
Build an Ironflow system using TypeScript with @ironflow/node that:
1. REACT: Create a function triggered by "order.placed" events with recording enabled. It should have three durable steps: - "validate-order" — validates the order - "process-payment" — charges the amount - "send-confirmation" — sends email confirmation
2. DERIVE: Create a managed projection called "order-stats" that listens to "order.placed" events and maintains a running total of orders and revenue.
3. Wire both into a createWorker and start it.
Use pull mode (createWorker), not push mode (serve).Enable recording: true on the function for time-travel debugging.Expected AI Response
Section titled “Expected AI Response”The AI should generate code like this:
import { createFunction, createProjection, createWorker, type IronflowProjection,} from "@ironflow/node";
interface OrderData { orderId: string; total: number; email: string;}
// REACT: A recorded function that processes orders// Every step is memoized and permanently recorded.const processOrder = createFunction( { id: "process-order", triggers: [{ event: "order.placed" }], recording: true, // Enables time-travel debugging }, async ({ event, step }) => { const data = event.data as OrderData;
const order = await step.run("validate-order", async () => { return { valid: true, orderId: data.orderId, total: data.total }; });
const payment = await step.run("process-payment", async () => { return { charged: true, amount: order.total, transactionId: `txn_${Date.now()}`, }; });
await step.run("send-confirmation", async () => { return { sent: true, email: data.email }; });
return { order, payment }; },);
// DERIVE: A projection that computes order statistics// Pure reducer — state is automatically persisted and queryable.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), }),});
// Start the worker — functions and projections run togetherconst worker = createWorker({ functions: [processOrder], projections: [orderStats as IronflowProjection],});
worker.start().then(() => { console.log("Worker started — listening for events");});package main
import ( "context" "fmt" "log" "os"
"github.com/sahina/ironflow-go/ironflow")
var processOrder = ironflow.CreateFunction(ironflow.FunctionConfig{ ID: "process-order", Mode: ironflow.PullMode, Triggers: []ironflow.Trigger{{Event: "order.placed"}}, Recording: true, // Enables time-travel debugging}, func(ctx ironflow.Context) (any, error) { var data struct { OrderID string `json:"orderId"` Total float64 `json:"total"` Email string `json:"email"` } if err := ctx.Event.Data(&data); err != nil { return nil, fmt.Errorf("parse order: %w", err) }
order, err := ironflow.Run(ctx, "validate-order", func() (map[string]any, error) { return map[string]any{"valid": true, "orderId": data.OrderID, "total": data.Total}, nil }) if err != nil { return nil, err }
payment, err := ironflow.Run(ctx, "process-payment", func() (map[string]any, error) { return map[string]any{"charged": true, "amount": data.Total}, nil }) if err != nil { return nil, err }
_, err = ironflow.Run(ctx, "send-confirmation", func() (map[string]any, error) { return map[string]any{"sent": true, "email": data.Email}, nil }) if err != nil { return nil, err }
return map[string]any{"order": order, "payment": payment}, nil})
// DERIVE: A projection that computes order statisticsvar orderStats = ironflow.CreateProjection(ironflow.ProjectionConfig{ Name: "order-stats", Events: []string{"order.placed"}, Mode: ironflow.ProjectionModeManaged, InitialState: func() map[string]any { 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) { total, _ := event.Data["total"].(float64) // float64, not int. The runner JSON round-trips state between events, // so every number comes back as a float64 — an `.(int)` assertion // silently yields 0 and the count never advances past 1. totalOrders, _ := state["totalOrders"].(float64) totalRevenue, _ := state["totalRevenue"].(float64) return map[string]any{ "totalOrders": totalOrders + 1, "totalRevenue": totalRevenue + total, }, nil },})
func main() { worker := ironflow.NewWorker(ironflow.WorkerConfig{ ServerURL: "http://localhost:9123", APIKey: os.Getenv("IRONFLOW_API_KEY"), Functions: []ironflow.Function{processOrder}, Projections: []ironflow.Projection{orderStats}, })
log.Println("Worker starting...") if err := worker.Run(context.Background()); err != nil { log.Fatal(err) }}For a Python system, ask for ironflow-py with ironflow.worker instead of @ironflow/node.
The AI should generate code like this:
import time
from ironflow.projection import create_projectionfrom ironflow.worker import Worker, function
# ── React: A function that processes orders ─────────────────────# Every step is memoized. If the process crashes, it resumes# from the last completed step. With recording enabled, every# step is also permanently recorded for time-travel debugging.@function(id="process-order", triggers=[{"event": "order.placed"}], recording=True)async def process_order(ctx): data = ctx.event.data
order = await ctx.step.run("validate-order", lambda: { "valid": True, "orderId": data["orderId"], "total": data["total"], })
payment = await ctx.step.run("process-payment", lambda: { "charged": True, "amount": order["total"], "transactionId": f"txn_{int(time.time() * 1000)}", })
await ctx.step.run("send-confirmation", lambda: {"sent": True, "email": data["email"]})
return {"order": order, "payment": payment}
# ── Derive: A projection that computes order statistics ─────────# Projections are pure reducers. Every time an "order.placed"# event is recorded, this reducer runs and the derived state# is automatically persisted and queryable.order_stats = create_projection( name="order-stats", events=["order.placed"], initial_state=lambda: {"totalOrders": 0, "totalRevenue": 0}, handler=lambda state, event, ctx: { "totalOrders": state["totalOrders"] + 1, "totalRevenue": state["totalRevenue"] + event.data.get("total", 0), },)
# ── Start the worker ────────────────────────────────────────────# run() owns the event loop: the first Ctrl+C drains, a second stops.if __name__ == "__main__": print("Worker started — listening for events") Worker(functions=[process_order], projections=[order_stats]).run()Step 4: Emit Events
Section titled “Step 4: Emit Events”With the server and worker running, emit events to build up history:
ironflow emit order.placed --data '{"orderId": "order-1", "total": 99.99, "email": "customer@example.com"}'ironflow emit order.placed --data '{"orderId": "order-2", "total": 49.50, "email": "another@example.com"}'ironflow emit order.placed --data '{"orderId": "order-3", "total": 149.00, "email": "third@example.com"}'Watch the worker terminal — you’ll see each event trigger the function and execute all three steps.
Step 5: Ask the AI to Query What Was Derived
Section titled “Step 5: Ask the AI to Query What Was Derived”How do I check the current state of the "order-stats" projection?Show me the curl command and explain what I should see.The AI should tell you:
curl -s -X POST http://localhost:9123/ironflow.v1.ProjectionService/GetProjection -H 'Content-Type: application/json' -d '{"name":"order-stats"}' | jq '.stateValue // .state'{ "totalOrders": 3, "totalRevenue": 298.49}No aggregation queries. The projection derived this state automatically from the recorded events.
Step 6: Rewind Time
Section titled “Step 6: Rewind Time”Every step of every function run was permanently recorded. Ask the AI:
How do I use Ironflow's time-travel debugging to replay a function run?The AI should point you to:
Dashboard: Open http://localhost:9123, navigate to Runs, click any completed run, and use the timeline scrubber to drag back through the execution.
CLI:
ironflow run listironflow inspect <run_id> --replayIn replay mode: →/l next frame, ←/h prev frame, g/G first/last, j/k navigate steps, Tab switch panels, q quit.
Step 7: Iterate with AI
Section titled “Step 7: Iterate with AI”Now ask the AI to extend the system with more Continuous History features:
Add error handling to the process-order function:1. If validation fails, throw a NonRetryableError2. Add retry configuration for the payment step3. Add a step.sleep of 1 second between payment and confirmationOr try adding event sourcing:
Create an entity stream for orders using Ironflow's event sourcing.Append "order.placed", "order.shipped", and "order.delivered" eventsto an entity stream keyed by orderId. Then show me how to read thefull history of a single order.Verification Checklist
Section titled “Verification Checklist”After the AI generates code, verify:
- Each step has a unique, descriptive ID
- Step functions are idempotent (safe to retry)
-
recording: trueis set on functions you want to time-travel debug - Event data types match your expected payload
- Projections use
initialStateas a function (not a plain object) - Error handling uses
NonRetryableErrorwhere appropriate
What Just Happened?
Section titled “What Just Happened?”You used an AI assistant to build a Continuous History system:
- Emit — Recorded events as permanent, immutable facts
- React — A durable function processed each event with memoized steps
- Derive — A projection automatically computed statistics from the event stream
- Rewind — Time-travel debugging let you replay any execution
The AI agent template ensures your assistant generates code that follows these patterns correctly.
What’s Next
Section titled “What’s Next”- Getting Started — The hands-on tutorial (no AI required)
- Agent Template — Full AI context template
- Common Pitfalls — Mistakes AI makes with Ironflow
- Event Sourcing — Entity streams and projections
- Time-Travel Debugging — Dashboard and CLI replay