- Workflows
- Execution Modes
Execution Modes
Ironflow supports two execution modes, push and pull, to handle different workload requirements. A pull worker can also run embedded in your app process:
| Mode | Description | Best For |
|---|---|---|
| Push | Ironflow sends HTTP requests to your endpoint | Serverless functions, short tasks |
| Pull | Your worker connects and pulls jobs over HTTP polling (JS, Go, Python). Each SDK has a streaming code path available but the default entry point is polling. | Long-running tasks, no timeout limits |
| Embedded | Pull-mode worker runs inside your app process (e.g., Next.js instrumentation.ts) | Single-service deployments, prototyping |
Push Mode (Default)
Section titled “Push Mode (Default)”In push mode, Ironflow sends HTTP POST requests to your registered endpoint. This works well with serverless platforms like Next.js, Lambda, or any HTTP server.
// app/api/ironflow/route.ts (Next.js App Router)import { serve } from "@ironflow/node";import { processOrder } from "@/functions/process-order";
export const POST = serve({ functions: [processOrder], signingKey: process.env.IRONFLOW_SIGNING_KEY,});serve() does not register the functions. Register them as push once per deploy with
createClient(...).registerFunction({ id, triggers, endpointUrl, recording }) for each
function. Use a one-off script or a GET route.
import "github.com/sahina/ironflow-go/ironflow"
func main() { handler := ironflow.Serve(ironflow.ServeConfig{ Functions: []ironflow.Function{ProcessOrder}, SigningKey: os.Getenv("IRONFLOW_SIGNING_KEY"), }) http.Handle("/api/ironflow", handler) http.ListenAndServe(":3000", nil)}# app.py — run with: uvicorn app:appfrom ironflow.serve import servefrom functions import process_order
app = serve([process_order]) # signing_key defaults to IRONFLOW_SIGNING_KEYserve() returns an ASGI app. Mount it under FastAPI or Starlette, or run it with
uvicorn. Register the functions as push once per deploy with
await ironflow.serve.register(functions, endpoint_url=...) (it is async; from a
script, wrap it in asyncio.run(...)).
How push mode works:
- Ironflow receives an event
- Ironflow makes an HTTP POST to your endpoint URL
- Your handler processes the request and returns a response
- Ironflow persists step results for durability
When to use push mode:
- Serverless deployments (Vercel, AWS Lambda, Cloud Functions)
- Tasks that complete within platform timeout limits
- Stateless processing
Pull Mode
Section titled “Pull Mode”In pull mode, your worker connects to Ironflow and pulls jobs. The default transport uses HTTP polling (JS, Go, Python): createWorker polls GET /api/v1/workers/{workerId}/jobs. Each SDK also ships an opt-in ConnectRPC bidirectional streaming worker — createStreamingWorker from @ironflow/node/worker-streaming, ironflow.NewStreamingWorker in Go, and StreamingWorker in Python — but polling is the wired default. This removes the push request timeout and is ideal for long-running tasks; the function’s own timeout is not applied in pull mode, and neither transport cancels a running job on a time limit.
import { createWorker } from "@ironflow/node";import { generateVideo } from "./functions/generate-video";
const worker = createWorker({ serverUrl: "http://localhost:9123", functions: [generateVideo], maxConcurrentJobs: 4,});
// start() blocks until the worker is stopped — it does not resolve once the// worker is connected. Put any setup code before it, not after.await worker.start();A function handed to a worker is registered as pull. Setting mode: "pull" in the function config states that intent:
ironflow.createFunction({ id: "generate-video", mode: "pull", ... }, handler);func main() { worker := ironflow.NewWorker(ironflow.WorkerConfig{ ServerURL: "http://localhost:9123", Functions: []ironflow.Function{GenerateVideo}, MaxConcurrentJobs: 4, })
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer cancel()
if err := worker.Run(ctx); err != nil { log.Fatal(err) }}A function handed to a worker is registered as pull. Setting Mode: "pull" in the function config states that intent:
ironflow.CreateFunction(ironflow.FunctionConfig{ID: "generate-video", Mode: "pull", ...}, handler)from ironflow.worker import Workerfrom functions import generate_video
worker = Worker( server_url="http://localhost:9123", functions=[generate_video], max_concurrent_jobs=4,)
# run() owns the event loop. SIGINT or SIGTERM starts a bounded drain.worker.run()Python has no mode field: passing the function to a Worker registers it as pull.
StreamingWorker from ironflow.worker is the opt-in ConnectRPC streaming worker.
How pull mode works:
- Worker connects to Ironflow over HTTP polling (every SDK has a streaming path available, but polling is the default)
- Worker registers its functions
- When events arrive, Ironflow assigns jobs to connected workers
- Worker processes jobs and reports results
- Polling/connection stays open for continuous processing
When to use pull mode
Section titled “When to use pull mode”Pull mode workers exist because push mode (HTTP POST to serverless) has real constraints: timeouts, cold starts, public endpoints, and no persistent state. Reach for a worker when any of these apply:
1. Long-running jobs (beyond your platform’s HTTP timeout) Video/audio processing, ML inference, PDF generation, large data exports, scientific compute. Serverless timeouts kill these — limits are platform-dependent (Vercel Hobby 10s, Vercel Pro 60s, Lambda 15min, etc.) — see your platform’s HTTP timeout. A worker runs as long as the job needs.
2. Private infrastructure access Workers run inside your VPC, data center, or on-prem environment and reach internal databases, legacy systems, and private APIs. You never need to expose an HTTP endpoint publicly — the worker dials out to Ironflow.
3. Heavy compute / GPU workloads Model training, embedding generation, video transcoding. Run on beefy hardware with GPUs that serverless platforms can’t provide.
4. Stateful workloads Warm caches, preloaded ML models, persistent DB connection pools, open Kafka consumers. Push mode cold-starts on every invocation; workers keep state resident.
5. Long-polling / streaming integrations Tail Kafka or Kinesis, maintain a WebSocket to a third party, consume SSE feeds. Workers stay connected as long as needed.
6. Projections and read-model builders Consume entity streams continuously to build materialized views. Inherently long-lived — a natural fit for workers.
7. Cost control at high volume For sustained high-throughput workloads, one always-on worker is cheaper than millions of serverless invocations.
8. Air-gapped or restricted environments When code must run on your hardware (compliance, data residency), the worker pulls jobs outbound — Ironflow never reaches in.
When NOT to use pull mode
Section titled “When NOT to use pull mode”Stick with push mode if:
- Tasks complete within your platform’s HTTP timeout (Vercel Hobby 10s, Lambda 15min, etc.)
- Traffic is bursty or sporadic (idle workers waste money)
- Your team already deploys to Vercel / Next.js / Lambda and doesn’t want to run long-lived processes
- You want the platform to handle scaling for you
Rule of thumb: outbound-only connection + long duration + stateful or private infrastructure = worker. Otherwise, use push mode.
Embedded Worker (Pull Mode in Next.js)
Section titled “Embedded Worker (Pull Mode in Next.js)”For simpler deployments, embed the pull-mode worker directly inside your Next.js application using the instrumentation.ts hook — no separate process needed.
export async function register() { if (process.env.NEXT_RUNTIME === "nodejs") { const { createFunction, createWorker } = await import("@ironflow/node");
const myFunction = createFunction( { id: "my-function", triggers: [{ event: "my.event" }], recording: true }, async ({ event, step }) => { return await step.run("process", async () => ({ done: true })); }, );
const worker = createWorker({ functions: [myFunction] }); worker.start().catch(console.error); }}The worker starts in the background when Next.js boots. Dynamic imports and the NEXT_RUNTIME guard keep Edge runtime compatible.
In Python, start the worker as a background task of your ASGI app, for example in a FastAPI lifespan:
import asynciofrom contextlib import asynccontextmanager
from fastapi import FastAPIfrom ironflow.worker import Workerfrom functions import my_function
@asynccontextmanagerasync def lifespan(app: FastAPI): worker = Worker(functions=[my_function]) task = asyncio.create_task(worker.start()) # start() does not return until the worker stops yield await worker.drain() await task
app = FastAPI(lifespan=lifespan)When to use embedded workers:
- Single-service deployments (no separate worker process)
- Prototyping and development
- Apps with moderate throughput where a dedicated worker fleet isn’t justified
See
examples/todo-web/for a complete example.
Triggering Events
Section titled “Triggering Events”Events trigger workflow execution. Use the SDK client to emit events from your application:
import { createClient } from "@ironflow/node";
const client = createClient({ serverUrl: "http://localhost:9123",});
// Fire and forgetawait client.emit("order.placed", { orderId: "ord_123", total: 99.99,});client := ironflow.NewClient(ironflow.ClientConfig{ ServerURL: "http://localhost:9123",})
client.Emit(ctx, "order.placed", map[string]any{ "orderId": "ord_123", "total": 99.99,})from protobuf.wkt import Structfrom ironflow import IronflowRPCfrom ironflow.rpc import v1
with IronflowRPC(server_url="http://localhost:9123") as rpc: rpc.events.emit(v1.TriggerRequest( event="order.placed", data=Struct.from_python({"orderId": "ord_123", "total": 99.99}), ))Via cURL
Section titled “Via cURL”curl -X POST http://localhost:9123/ironflow.v1.IronflowService/Emit \ -H "Content-Type: application/json" \ -d '{ "event": "order.placed", "data": { "orderId": "ord_123", "total": 99.99 } }'Mode Comparison
Section titled “Mode Comparison”| Feature | Push Mode | Pull Mode |
|---|---|---|
| Timeout | min(function timeout, engine.pushTimeout). The server’s pushTimeout (default 10s) is a hard ceiling — a per-function timeout can only tighten it, never raise it — and your host platform’s own HTTP timeout (Vercel Hobby 10s, Pro 60s, Lambda 15min) bounds it further | Unlimited — the per-function timeout is not applied in pull mode |
| Scaling | Platform handles | You manage workers |
| Cold starts | Yes | No (worker process stays up) |
| Network | Outbound from Ironflow | Outbound from worker |
| Use case | Short tasks, serverless | Long tasks, GPU workloads |
Real-Time Event Subscriptions
Section titled “Real-Time Event Subscriptions”Subscribe to events in real-time using WebSocket-based subscriptions with NATS-style wildcard patterns.
Full documentation: See the Events & Pub/Sub guide for comprehensive SDK guides, error handling, reconnection patterns, and best practices.
Hybrid Event Model
Section titled “Hybrid Event Model”Ironflow supports two parallel event processing layers:
| Layer | Purpose | Use When |
|---|---|---|
| Function Layer | Durable workflow execution with retries, steps, state | Processing orders, sending emails, reliable webhooks |
| Event Stream Layer | Lightweight real-time distribution | Dashboards, monitoring, analytics, debugging |
Events are published to the Event Stream immediately, regardless of whether functions match.
Quick Start
Section titled “Quick Start”import { ironflow } from "@ironflow/browser";
ironflow.configure({ serverUrl: "http://localhost:9123" });
await ironflow.connect();
const sub = await ironflow.subscribe("system.run.*", { onEvent: (event) => console.log(event), onError: (error) => console.error(error.message), replay: 10,});
// Cleanupsub.unsubscribe();ironflow.disconnect();subClient := ironflow.NewSubscriptionClient(ironflow.SubscriptionClientConfig{ WSURL: "ws://localhost:9123/ws",})
ctx := context.Background()if err := subClient.Connect(ctx); err != nil { log.Fatal(err)}
sub, _ := subClient.Subscribe(ctx, ironflow.Patterns.AllRuns(), &ironflow.SubscribeOptions{ Replay: 5,})
for event := range sub.Events() { fmt.Printf("%s: %s\n", event.Topic, string(event.Data))}from ironflow import IronflowRPCfrom ironflow.rpc.v1 import SubscribeOptions, SubscribeRequest
with IronflowRPC(server_url="http://localhost:9123") as rpc: for event in rpc.pubsub.subscribe(SubscribeRequest( pattern="system.run.>", options=SubscribeOptions(replay=5), )): print(f"{event.topic}: {event.data.to_python()}")Python subscribes over the ConnectRPC server stream, not a WebSocket. Leave the loop to cancel.
ironflow subscribe "system.run.>" --replay 10Pattern Syntax
Section titled “Pattern Syntax”| Token | Description | Example |
|---|---|---|
* | Matches one segment | system.run.*.created |
> | Matches one+ segments (end only) | system.run.> |
Common patterns: system.run.> (all runs), system.run.{id}.step.> (steps), events:> (user events)
What’s Next?
Section titled “What’s Next?”- Error Handling — Handle errors with NonRetryableError
- Debugging — Hot patching, TUI debugger, VS Code DAP