Workflows
Ironflow executes workflows durably, records events, derives projections, and lets you time-travel through execution history. Workflows survive crashes, retries, and restarts automatically; completed steps are memoized, and with recording enabled every step becomes a recorded fact.
Key Concepts
Section titled “Key Concepts”| Concept | Description |
|---|---|
| Function | A workflow definition that responds to events. Contains one or more steps. |
| Step | A recorded fact within a function. Steps are memoized—if a workflow restarts, completed steps aren’t re-executed. |
| Event | A recorded fact that triggers a workflow (e.g., order.placed, user.signup). Kept until you prune or delete it. |
| Run | A single execution of a function, triggered by an event. |
How it works:
- You define functions with steps using the SDK
- Register functions with the Ironflow server (push or pull mode)
- Trigger events from your application
- Ironflow executes functions, memoizing each step result and — with
recording: true— recording it as a fact, enabling replay, debugging, and history navigation
Real-time events: For subscribing to workflow events in real-time, see the Events & Pub/Sub guide.
Run Status
Section titled “Run Status”Automatic sleep, event, child-invocation, retry, and recovery waits report
waiting. An execution segment awaiting capacity reports
waiting_for_capacity. An explicit operator hold reports paused.
A run that used up its retry attempts on retryable failures also reports paused,
with pause reason retries_exhausted and the last error attached. That is the
default; a function (or spec.engine.retry.onExhausted) can choose fail instead.
A paused run is not over: it keeps its memoized steps, holds no capacity, and waits
for ironflow run resume, which grants it a fresh retry budget. See
Error handling.
You can pause or cancel a waiting run. Resume removes an operator hold and preserves the original condition and deadline: an unresolved wait returns to waiting, and a completed condition continues automatically. An event that arrives before the deadline during the hold stays pending and applies shortly after Resume.
Installation
Section titled “Installation”The @ironflow/* npm packages are public and install without authentication.
npm install @ironflow/node# orpnpm add @ironflow/nodeThe Go SDK is publicly available via GitHub:
go get github.com/sahina/ironflow-go/ironflow@latestpip install ironflow-pyThe Python SDK defines functions with ironflow.worker (pull mode) and
ironflow.serve (push mode, ASGI). See the
Python SDK reference.
Local Development
Section titled “Local Development”# Build first. `embed` matters: a plain `make build` produces a binary whose# `serve` exits "embedded dashboard missing (static/index.html not found)".make embed build
# Start Ironflow server./build/ironflow serve
# Dashboard: http://localhost:9123# API: http://localhost:9123/api/v1Register your function endpoint in the dashboard or via API, then trigger events to test.
Quick Start
Section titled “Quick Start”Here’s a minimal workflow to get started:
import { ironflow } from "@ironflow/node";
export const helloWorld = ironflow.createFunction( { id: "hello-world", triggers: [{ event: "hello.triggered" }], }, async ({ event, step }) => { const message = await step.run("create-message", async () => { return `Hello, ${event.data.name}!`; });
return { message }; },);import "github.com/sahina/ironflow-go/ironflow"
var HelloWorld = ironflow.CreateFunction(ironflow.FunctionConfig{ ID: "hello-world", Triggers: []ironflow.Trigger{{Event: "hello.triggered"}},}, func(ctx ironflow.Context) (any, error) { var data struct { Name string `json:"name"` } if err := ctx.Event.Data(&data); err != nil { return nil, err }
message, err := ironflow.Run(ctx, "create-message", func() (string, error) { return fmt.Sprintf("Hello, %s!", data.Name), nil }) if err != nil { return nil, err }
return map[string]string{"message": message}, nil})from ironflow.worker import Worker, function
@function(id="hello-world", triggers=[{"event": "hello.triggered"}])async def hello_world(ctx): message = await ctx.step.run("create-message", lambda: f"Hello, {ctx.event.data['name']}!") return {"message": message}
Worker(functions=[hello_world]).run()Triggering Workflows
Section titled “Triggering Workflows”Workflows run when a matching event is sent. You can trigger events from your application code, the CLI, or the REST API.
import { createClient } from "@ironflow/node";
const client = createClient({ serverUrl: "http://localhost:9123" });
// Fire-and-forgetawait client.emit("hello.triggered", { name: "Alice" });
// Wait for every run the event triggersconst results = await client.emitSync("hello.triggered", { name: "Alice" });
// Or target one function by ID and wait for its single resultconst result = await client.invoke("hello-world", { data: { name: "Alice" } });client := ironflow.NewClient(ironflow.ClientConfig{ ServerURL: "http://localhost:9123",})
// Fire-and-forgetclient.Emit(ctx, "hello.triggered", map[string]any{"name": "Alice"})
// Wait for every run the event triggersresults, err := client.EmitSync(ctx, "hello.triggered", map[string]any{"name": "Alice"}, 30*time.Second)
// Or target one function by ID and wait for its single resultresult, err := client.InvokeSync(ctx, "hello-world", map[string]any{"name": "Alice"}, 30*time.Second)from protobuf.wkt import Structfrom ironflow import IronflowClient, IronflowRPCfrom ironflow.rpc import v1
client = IronflowClient(server_url="http://localhost:9123")
# Fire-and-forgetwith IronflowRPC(server_url=client.server_url, api_key=client.api_key) as rpc: res = rpc.events.emit(v1.TriggerRequest(event='hello.triggered', data=Struct.from_python({'name': 'Alice'})))print(res.event_id, res.run_ids)
# No emitSync on the REST client — TriggerSync is ConnectRPC-only (rpc.runs.trigger_sync). To poll instead:rpc = IronflowRPC(server_url=client.server_url, api_key=client.api_key)run = rpc.runs.get(v1.GetRunRequest(id=res.run_ids[0]))print(run.status)Python also exposes the asynchronous invocation RPC, which skips event matching but returns immediately with a run id to poll:
from ironflow import IronflowRPCfrom ironflow.rpc.v1 import InvokeFunctionRequestfrom protobuf.wkt import Struct
rpc = IronflowRPC()rpc.functions.invoke(InvokeFunctionRequest(function_id="hello-world", data=Struct.from_python({"name": "Alice"})))The blocking form is on the generated ConnectRPC client rather than the REST
one — IronflowRPC.runs.invoke_function_sync — matching Go’s InvokeSync and
the JavaScript invoke().
# Fire-and-forgetironflow emit hello.triggered --data '{"name": "Alice"}'
# Wait for the run to completeironflow emit hello.triggered --data '{"name": "Alice"}' --waitcurl -X POST http://localhost:9123/ironflow.v1.IronflowService/Emit \ -H "Authorization: Bearer $IRONFLOW_API_KEY" \ -H "Content-Type: application/json" \ -d '{"event": "hello.triggered", "data": {"name": "Alice"}}'You can also invoke a function directly by ID, bypassing event matching. Every SDK has a blocking form — InvokeSync in Go, invoke() in Node and the browser, rpc.runs.invoke_function_sync on the Python IronflowRPC client — over the InvokeFunctionSync RPC; it returns exactly one result and raises on a failed run. For the non-blocking form see the REST API reference for the InvokeFunction RPC. The split between the two is recorded in ADR 0067.
The underlying gRPC IronflowService exposes the event-ingestion RPCs SDKs and tooling use directly: Trigger and Emit (fire-and-forget event publish), TriggerSync (publish and wait for every matched run to complete), InvokeFunctionSync (run one function by ID and wait for its single result), TriggerBatch (batch publish; a failed item reports its error and the other items still emit), and PatchStep (scoped injection — overwrite a completed step’s output and resume). See the generated api/ironflow/v1 package on the public Go mirror for the full service definition.
For more details on event types, namespaces, and pattern matching, see the Events guide. SDK-specific options (idempotency keys, metadata, timeouts) are covered in the Node.js SDK, Browser SDK, and Go SDK references.
What’s Next?
Section titled “What’s Next?”- Defining Functions — Learn about function configuration and triggers
- Step Primitives — Understand run, sleep, sleepUntil, waitForEvent, parallel, map, compensate, invoke, invokeAsync, and publish
- Execution Modes — Push mode vs. Pull mode
- Error Handling — NonRetryableError and signature verification
- Sagas & Compensation — Automatically undo completed steps on failure
- Debugging — Hot patching, scoped injection, time-travel debugging, TUI debugger, VS Code DAP
- API Reference — REST API, Events API, WebSocket