A lightweight, high-performance workflow engine built in Rust with Lua scripting. The open-source alternative to n8n, Airflow, and Prefect that ships as a single binary without a Python, Node.js, or container runtime.
IronFlow is a DAG-based workflow orchestration engine designed for CI/CD pipelines, data processing, ETL jobs, API integrations, document extraction, and task automation. Define workflows in simple Lua scripts — a language with only ~20 keywords — and run them on a sandboxed, async Rust runtime with parallel step execution, retry logic, conditional routing, and a built-in REST API.
No Python. No Node.js. No Docker required. Most workflows need only the IronFlow binary; the pdf_to_image and pdf_thumbnail nodes additionally require a native Pdfium library. IronFlow runs on Linux, macOS, edge servers, and air-gapped environments.
local flow = Flow.new("process_order")
flow:step("validate", nodes.validate_schema({
source_key = "order",
schema = {
type = "object",
required = { "order_id", "amount" },
properties = {
order_id = { type = "string" },
amount = { type = "number" }
}
}
}))
flow:step("charge", nodes.http_post({
url = "https://payments.example.com/charge",
body = { amount = "${ctx.order.amount}", order_id = "${ctx.order.order_id}" },
auth = { type = "bearer", token = env("PAYMENT_API_KEY") }
})):depends_on("validate"):retries(3, 1.0)
flow:step("notify", nodes.log({
message = "Order ${ctx.order.order_id} charged successfully",
level = "info"
})):depends_on("charge")
return flowironflow run process_order.lua --context '{"order": {"order_id": "ORD-42", "amount": 99.99}}'For teams that can't use n8n, Dify, or hosted workflow platforms — whether due to company policy, air-gapped environments, or the need for something faster and simpler.
IronFlow gives you a workflow engine that:
- Ships as a single binary — no Python, Node.js, package manager, or container runtime required; PDF rendering nodes require Pdfium
- Runs anywhere — Linux, macOS, CI/CD pipelines, edge servers, embedded systems
- Is fast — Rust-powered execution with parallel step scheduling via DAG resolution
- Is safe — Sandboxed Lua can't access the filesystem or OS unless you explicitly allow it
- Is easy to learn — Lua has ~20 keywords. If you can write JSON, you can write flows
Rust as the runtime + Lua as the scripting layer. A well-proven pattern used by Neovim, OpenResty/Nginx, Redis, and game engines like Roblox.
┌─────────────────────────────────────────────────────────┐
│ Lua Flow Scripts │
│ flow:step("name", nodes.http_get({...})) │
│ flow:step("process", function(ctx) ... end) │
├─────────────────────────────────────────────────────────┤
│ IronFlow Engine │
│ DAG resolution · Parallel execution · Retry/timeout │
│ Context propagation · Conditional routing · State store │
├─────────────────────────────────────────────────────────┤
│ 102 Built-in Nodes │
│ HTTP · Files · Shell · Transforms · Conditionals · ... │
│ All implemented in pure Rust for performance & safety │
└─────────────────────────────────────────────────────────┘
| What | How |
|---|---|
| Flow definitions | Lua scripts — easy to write, read, and modify |
| Node implementations | Pure Rust — fast, memory-safe, no GC pauses |
| Shared context | Lua table backed by Rust HashMap, serialized as JSON |
| DAG resolution | Topological sort with cycle detection (Kahn's algorithm) |
| Parallel execution | Steps without dependencies run concurrently via Tokio |
| State persistence | Rust trait with JSON and SQLite backends; optional PostgreSQL and Redis features |
- 103 built-in nodes — HTTP (GET/POST/PUT/DELETE), file I/O, ZIP utilities (
zip_create,zip_list,zip_extract), S3 operations, shell commands, JSON/CSV/XML/YAML transforms, foreach iteration, key-value caching (memory + file), conditional routing, schema validation, hashing, templating, Markdown conversion, HTML sanitization, document extraction (Word/PDF/PPTX/HTML/VTT/SRT/Excel), PDF merge/split, database queries (SQLite via sqlx, ArangoDB via HTTP), AI text embeddings/chunking (ai_*) and chat/completions (llm) across providers, audio/video transcription (transcribe) via OpenAI, OpenAI-compatible, or Azure, an MCP 2025-11-25 client over persistent stdio and Streamable HTTP (mcp_client), notification integrations (send_email,slack_notification), data extraction helpers (json_extract_path,if_body_contains,if_http_status), delays, inline code execution, bounded repeated subworkflow composition, LLM tool dispatch (tool_dispatch), presigned S3 URL support, base64 encoding/decoding, date formatting, image helpers (pdf_to_image,pdf_thumbnail,image_to_pdf,image_resize,image_crop,image_rotate,image_flip,image_grayscale,image_metadata,image_convert,image_watermark,pdf_metadata). - Function handlers — pass Lua functions directly as step handlers, no boilerplate needed
- Conditional step shorthand —
step_if(condition, name, handler)for concise branching - DAG-based scheduling — steps run in parallel unless dependencies are declared
- Retry with exponential backoff — configurable per step
- Total per-step timeouts — one budget across attempts and retry backoff, with process-group cleanup on Unix
- Conditional routing —
if_nodeandswitch_nodefor branching workflows - Context interpolation — documented node parameters support explicit context paths such as
${ctx.user.name},${ctx.items[0].name}, and${ctx["key.with.dots"]} - Lua globals —
env(),uuid4(),now_rfc3339(),now_unix_ms(),json_parse(),json_stringify(),log(),base64_encode(),base64_decode() - Schema validation — JSON Schema validation to fail fast on bad input
- REST API — run and manage flows over HTTP (Axum-based)
- CLI — run, validate, inspect, and list workflows from the terminal
- Planned error recovery —
on_errorschedules a dedicated recovery step inside the validated DAG, with normal dependencies, retries, and timeouts - Subworkflow composition — call reusable
.luaflows once, fan out runtime work withparallel_subworkflows, or iterate explicit state with boundedrepeat_subworkflow - Sandboxed execution — Lua scripts run without
os,io, ordebugaccess
git clone https://github.com/skitsanos/ironflow.git
cd ironflow
cargo build --release# Simple flow
./target/release/ironflow run examples/01-basics/hello_world.lua --context '{"user_name": "Alice"}'
# Call OpenAI and extract the reply with a function handler
./target/release/ironflow run examples/05-http/openai_with_extract.lua --context '{"prompt": "Explain recursion"}'
# Validate without executing
./target/release/ironflow validate examples/03-control-flow/switch_routing.lua
# Verbose mode — see per-task timing and outputs
./target/release/ironflow run examples/07-advanced/data_pipeline.lua --verbose./target/release/ironflow serve --host 127.0.0.1 --port 3000# Run a flow via API
curl -X POST http://127.0.0.1:3000/flows/run \
-H "Content-Type: application/json" \
-d '{
"source": "local flow = Flow.new(\"hello\")\nflow:step(\"greet\", nodes.log({ message = \"Hello ${ctx.user}!\" }))\nreturn flow",
"context": {"user": "Alice"}
}'
# Or send base64-encoded Lua to avoid JSON escaping
curl -X POST http://127.0.0.1:3000/flows/run \
-H "Content-Type: application/json" \
-d '{
"source_base64": "bG9jYWwgZmxvdz1GbG93Lm5ldygiaGVsbG8iKTtmbG93OnN0ZXAoImdyZWV0Iixub2Rlcy5sb2coe21lc3NhZ2U9IkhlbGxvICR7Y3R4LnVzZXJ9ISJ9KSk7cmV0dXJuIGZsb3c=",
"context": {"user": "Alice"}
}'For settings with multiple sources, IronFlow resolves values as explicit CLI
argument > existing process environment > selected dotenv file >
ironflow.yaml > built-in default. An explicit CLI value still wins when it is
equal to the default, and dotenv never replaces a variable already supplied by
the shell, container, or service manager.
Without --dotenv, IronFlow checks only .env in the current working
directory and silently continues when it is absent. An explicit dotenv file,
or a discovered default file that exists, must be readable and valid. The
whole file is parsed before any values are applied, tracing, or the final
source-aware CLI parse, so dotenv can configure RUST_LOG, CLI environment aliases,
runtime limits, and Lua env() calls without partial startup state.
The repository's .env.example contains safe starter values.
See the CLI reference for the
complete contract and configuration variables.
Extraction CPU, RSS, output, persistence, concurrency, and cancellation-drain
trends can be measured with the opt-in
release subprocess benchmark. Local calibration
documents stay in ignored data/samples/; benchmark results are not timing
gates for ordinary CI.
| Command | Description |
|---|---|
ironflow run <file> |
Execute a workflow |
ironflow validate <file> [--strict] |
Validate a flow without running; strict mode rejects Lua handler warnings |
ironflow nodes |
List all available node types |
ironflow list |
List a bounded page of past workflow runs (--limit, --after) |
ironflow inspect <run_id> |
Inspect a specific run |
ironflow artifacts prune |
Offline bounded cleanup of artifacts not referenced by retained runs |
ironflow serve |
Start the REST API server |
| Method | Path | Description |
|---|---|---|
POST |
/flows/run |
Execute a flow; optional Idempotency-Key makes retries converge on one durable run |
POST |
/flows/validate |
Validate a flow |
GET |
/runs |
List run summaries with status, limit, and after cursor parameters |
GET |
/runs/{id} |
Get run details |
GET |
/runs/{id}/events |
Stream run/task lifecycle events over SSE |
DELETE |
/runs/{id} |
Delete run state and retained events (409 while a non-terminal owner lease is live) |
GET |
/nodes |
List available nodes |
POST |
/webhooks/{name} |
Execute a webhook-mapped flow |
GET |
/health |
Backwards-compatible liveness check |
GET |
/health/live |
Process liveness |
GET |
/health/ready |
Admission and durable-store readiness |
GET |
/metrics |
Opt-in authenticated operator metrics (IRONFLOW_METRICS_ENABLED=true) |
The metrics endpoint exports a fixed-cardinality, process-local OpenMetrics contract for run/task outcomes and durations, active work, admission, scheduling, leases, and storage failures. It follows the API authentication boundary and is absent when disabled. See Operator metrics.
Flows can also run on a schedule. A schedules: block in ironflow.yaml
declares bounded standard five-field cron triggers that ironflow serve
evaluates—in a named time zone, with at most one replica claiming a given
instant. If day-of-month and weekday are both restricted, either match fires
the schedule, as in traditional crontab. A claimed instant may still
be skipped for lateness, overlap, or capacity; overlap suppression is a bounded
best-effort check rather than a distributed lock. Schedule names evaluate
concurrently under a 15-second per-tick budget, and the scheduler task is
supervised with the API server: an unexpected scheduler exit stops serve
instead of leaving an apparently healthy API with no triggers. JSON and SQL
claim retention is schedule-scoped, cadence-limited, and capped at 256 records
per cleanup pass; Redis uses per-claim TTL. See
Schedules.
Active-active deployments must set IRONFLOW_REPLICA_MODE=true and use
PostgreSQL or Redis for both state and events. SIGTERM closes readiness and new
execution admission before a bounded drain. See the
replica deployment contract and its opt-in
two-process Docker fault gate.
Handler failures return JSON with error and a stable code. Internal failures
return only a generic message plus an opaque error_id, also exposed as
X-Error-ID for correlation with sanitized server logs. Full error chains and
connection credentials are never included in the response.
Public run IDs are opaque 1–128 byte ASCII tokens: they must start and end with
a letter or digit, and may contain letters, digits, -, or _ between them.
IronFlow-generated IDs are UUIDv4 strings. Malformed IDs are rejected with
400 bad_request; reading a valid but unknown ID returns 404 not_found.
Deleting one also returns 404 unless retained orphan events are recovered.
Run listings are ordered by start time at microsecond precision, newest first,
with missing timestamps last and descending run ID as the deterministic
tie-breaker. Both the API and CLI enforce IRONFLOW_MAX_LIST_RECORDS (default
100); there is no unbounded all mode. The API normally returns 50 records
(or the configured cap when lower), and next_cursor / --after reaches the
following page.
The cursor listing contract is an intentional next-major-version boundary.
GET /runs replaces offset with after and no longer returns an exact
total; ironflow list --format json now returns a summary page envelope
instead of an array of full run records. API, CLI, and Rust-library consumers
must migrate together. See the CLI reference
for the exact old-to-new mapping.
The default JSON state backend rejects symlinked store roots, run records, and
summary entries, and publishes each file through a same-directory temporary file.
The main record is authoritative: main and summary files carry the same opaque
revision and SHA-256 digest of the public summary. Listing accepts a sidecar
only when a bounded primary header matches both values and the sidecar content
recomputes to that digest. Missing, syntactically invalid, schema-unusable, or
revision/digest-mismatched sidecars fall back to the full primary and are
best-effort repaired; a sidecar with an explicit string run ID that disagrees
with its filename is corruption and does not fall back. The bounded fast path
intentionally does not decode the remainder of a primary whose committed header
and sidecar agree; full reads and mutations still validate the complete primary
record.
Bounded pages use an immutable checksummed fixed-record base with global and
per-status sections plus a checksummed, coalesced delta capped at 128 distinct
run IDs. Clean cursor pages binary-search the base, range-read the requested
window plus the bounded overlay, and merge both without enumerating the store
directory: O(log N + page size + K) reads and O(page size + K) memory, where
K <= 128. Participating local writers mark the projection dirty before a
primary change and coordinate through one file lock. Initialization, status
changes, and deletion normally replace only the O(K) delta; the 129th distinct
overlay ID performs an O(N) compaction into a new base and resets the delta.
Task/context-only updates leave both files unchanged. A version-2 clean token
binds the base generation and delta revision, so missing, dirty, stale, or
malformed metadata rebuilds automatically from authoritative primaries. Stop
all writers when upgrading or downgrading across this state format. For an
explicit offline repair and compaction, stop writers and call
JsonStateStore::rebuild_run_summary_catalog(). The JSON backend remains a
moderate-cardinality local store; prefer SQL or Redis for sustained high-write
workloads.
On Unix, its directory is mode 0700 and committed files are 0600; other
platforms require equivalent operator-managed ACLs. See the precise filesystem
and atomicity boundary in
Architecture.
Run-event SSE supports an exclusive ?after=<event_id> cursor and standard
Last-Event-ID reconnection, drains complete backend pages, and closes after
the terminal event. Failures found before streaming use the JSON contract
above; failures after 200 use one ID-less stream_error frame and then close
without advancing the replay cursor. See the complete client contract in the
CLI reference.
Every logical event must have a non-empty ID that is never assigned to another
event in the same run; the engine-generated UUIDv4 IDs satisfy this obligation.
An exact publication retry is idempotent while the backend retains that event
identity. Bounded or TTL-backed stores can detect conflicting ID reuse only
while the prior identity remains retained. Unknown, expired, and cross-run
event cursors return storage NotFound and become 410 event_cursor_gone at
the HTTP boundary.
The default in-memory event backend retains events and deletion fences in one
oldest-first queue across all runs. It enforces both a 10,000-entry limit and a
fixed 64 MiB retained-heap estimate so large metadata cannot bypass the count
bound; set IRONFLOW_EVENT_MEMORY_CAPACITY or event_memory_capacity to
another positive entry limit. DELETE /runs/{id} removes state first, then
idempotently removes retained events and fences late event publication.
Repeating the request can finish event cleanup left by an interrupted first
attempt. A non-terminal run with an unexpired execution-owner lease is left
intact and returns 409 conflict; terminal and expired-lease runs are
deletable.
Redis legacy event migration requires Redis 6.2 or newer. It atomically moves
an eligible family into deterministic exact-run quarantine, then validates it
twice through head batches of at most 128 events and 1 MiB returned to Rust.
Persisted generations and pending intents make same-list LMOVE rotations
resumable without changing final event order. One operation confirms at most
32 bounded steps before returning a typed conflict that asks the caller to
retry. Interrupted requests and processes resume saved progress; a missing
state record leaves the deterministic snapshot intact and fails closed.
Unsafe run IDs are handled automatically only with an exact owner marker: an encoded current family is accepted, while an owned raw family can migrate. An ambiguous ownerless encoded family requires manual recovery; an optional raw candidate without that exact owner is preserved and ignored. Redis must read one oversized list element once because it has no element-length metadata; validation then fails closed and boundedly restores the original family. Forward and reverse batches both enforce the 1 MiB aggregate limit. The shortest source TTL is applied to every quarantined component immediately; expired quarantine is released on the next access without reviving retained events. Stop pre-protocol event writers during migration. See the full ownership, recovery, and TTL boundaries in Architecture.
SQL event IDs are scoped to their run. Upgrading a legacy SQL event table whose
primary key is only id requires a coordinated stop of older event writers;
after (run_id, id) permits cross-run ID reuse, downgrading needs an offline
data transformation. See the storage upgrade boundary in
Architecture.
local flow = Flow.new("my_pipeline")
-- Steps without dependencies run in parallel
flow:step("fetch_users", nodes.http_get({
url = "https://api.example.com/users",
output_key = "users"
}))
flow:step("fetch_orders", nodes.http_get({
url = "https://api.example.com/orders",
output_key = "orders"
}))
-- This step waits for both
flow:step("merge", nodes.log({
message = "Got users ${ctx.users_data} and orders ${ctx.orders_data}"
})):depends_on("fetch_users", "fetch_orders")
return flowEach DAG phase reads one immutable phase-start context snapshot, so independent
steps cannot consume one another's output. Their outputs are committed after
the phase settles, in flow declaration order; if two steps publish the same
key, the later-declared step wins. Use depends_on() when data must flow from
one step to another, and distinct output_key prefixes when parallel results
must coexist. Parallel event, log, and external-side-effect order remains
timing-dependent.
Node documentation identifies which string parameters support context interpolation. Supported parameters use path lookup, not Lua expressions:
nodes.template_render({
template = 'User ${ctx.user.name} chose ${ctx.items[0].name} (${ctx["display.label"]})',
output_key = "summary"
})Array indexes are zero-based, including in Lua flow strings. Dot notation is for identifier-like object keys; JSON double-quoted bracket notation accesses keys containing dots or other punctuation. Function calls, operators, and fallback expressions are not part of the grammar. Compute those values in an explicit function/code step first.
Missing and null values render as an empty string. Other ${...} forms such
as ${HOME} remain literal for tools such as shells to interpret. To emit a
literal ${ctx.value}, prefix it with a backslash at runtime; in a Lua string,
write "\\${ctx.value}".
Write inline Lua logic as step handlers — no need for nodes.code():
flow:step("transform", function(ctx)
local total = 0
for _, item in ipairs(ctx.items) do
total = total + item.price * item.qty
end
return { order_total = total }
end):depends_on("load_items")Handlers are serialized into an isolated Lua VM. Keep their local state inside
the function or pass values through ctx; captured outer locals are rejected.
Validation reports undefined handler and string-backed code globals as
source-positioned warnings, and ironflow validate flow.lua --strict treats
those warnings as failures. It also compiles string-backed code during
validation, without executing it, so invalid syntax fails immediately.
flow:step("call_api", nodes.http_post({
url = "https://unreliable-api.example.com/submit",
body = { data = "${ctx.payload}" },
timeout = 10
})):retries(3, 1.0):timeout(30)
-- 3 retries with 1s → 2s → 4s exponential backoff
-- 30s total execution budget across all attempts and backoffThe step timeout does not reset for each retry. When it expires, the active
async node future is dropped, code/foreach and nested-flow Lua evaluation
are interrupted through their execution hooks, owned shell subprocesses are
terminated, and an active MCP session is invalidated and cleaned up. MCP stdio
cleanup also stops its owned server process. Durable state writes used to
record the outcome are not forcibly interrupted by that deadline. A timeout
cannot roll back an external side effect that already completed; see
Lua Flow Guide — Timeout for the exact
cancellation boundary.
-- Simple: step_if runs the step only when the condition is true
flow:step_if("ctx.amount > 100", "vip_discount", nodes.code({
source = "return { discount = ctx.amount * 0.1 }"
}))
-- Full control: if_node + route for multi-branch workflows
flow:step("check", nodes.if_node({
condition = "ctx.amount > 100",
true_route = "premium",
false_route = "standard"
}))
flow:step("premium_flow", nodes.log({
message = "VIP: $${ctx.amount}"
})):depends_on("check"):route("premium")
flow:step("standard_flow", nodes.log({
message = "Standard: $${ctx.amount}"
})):depends_on("check"):route("standard")| Category | Nodes |
|---|---|
| HTTP | http_request, http_get, http_post, http_put, http_delete |
| Files | read_file, write_file, copy_file, move_file, delete_file, list_directory |
| S3 | s3_presign_url, s3_get_object, s3_put_object, s3_delete_object, s3_copy_object, s3_list_objects, s3_list_buckets |
| S3 Vectors | s3vector_create_bucket, s3vector_get_bucket, s3vector_delete_bucket, s3vector_create_index, s3vector_get_index, s3vector_delete_index, s3vector_put_vectors, s3vector_query_vectors, s3vector_delete_vectors |
| Shell | shell_command |
| Transforms | json_parse, json_stringify, json_extract_path, csv_parse, csv_stringify, select_fields, rename_fields, data_filter, data_transform, batch, deduplicate, foreach |
| Conditionals | if_node, if_body_contains, if_http_status, switch_node |
| Validation | validate_schema, json_validate |
| Markdown | markdown_to_html, html_to_markdown |
| Cache | cache_set, cache_get |
| Notification | send_email, slack_notification |
| Database | db_query, db_exec, arangodb_aql |
| Composition | subworkflow, parallel_subworkflows, repeat_subworkflow, tool_dispatch, code |
| XML | xml_parse, xml_stringify |
| YAML | yaml_parse, yaml_stringify |
| HTML | html_sanitize |
| Encoding | base64_encode, base64_decode |
| Date/Time | date_format |
| Utility | log, hash, delay, template_render |
| ZIP | zip_create, zip_list, zip_extract |
| MCP | mcp_client |
| AI | ai_embed, ai_chunk, ai_chunk_merge, ai_chunk_semantic, llm, transcribe |
| Extraction | extract_word, extract_pdf, extract_pptx, extract_html, extract_vtt, extract_srt, extract_xlsx, pdf_to_image, pdf_thumbnail, pdf_metadata, image_to_pdf, pdf_merge, pdf_split |
| Image Processing | image_resize, image_crop, image_rotate, image_flip, image_grayscale, image_metadata, image_convert, image_watermark |
See docs/NODE_REFERENCE.md for the complete reference with parameters and examples.
Progressive examples from basic to advanced:
| Folder | What you'll learn |
|---|---|
| 01-basics | Logging, context passing, parallel execution, retries, env vars, Lua globals |
| 02-data-transforms | JSON parse/stringify, filtering, batching, deduplication |
| 03-control-flow | If/else routing, switch/case routing, step_if shorthand |
| 04-file-operations | Read, write, copy, move, delete, list |
| 05-http | API calls, authentication, OpenAI integration |
| 06-shell | Shell commands with args, env vars, timeouts, and explicit exit-status policy |
| 07-advanced | Hashing, schema validation, full data pipelines, function handlers, base64 encoding |
| 08-extraction | Word/PDF/PPTX/HTML/VTT/SRT/Excel extraction, metadata, PDF-to-image rendering, image resize/crop, PDF merge/split, image metadata |
| 09-cache | In-memory and file-based key-value caching with TTL |
| 10-database | SQLite CRUD operations with db_query and db_exec |
| 11-subworkflow | Subworkflow composition, fire-and-forget, on_error handling |
| 12-arangodb | ArangoDB AQL queries with bind variables and env-based credentials |
| 13-ai | Text embeddings (OpenAI, Ollama, OAuth), text chunking (fixed, split, merge, semantic) |
| 14-notifications | Email via Resend or SMTP, Slack webhooks |
| 15-webhooks | Config-driven webhook routes with default-deny, execution-only signature headers |
| 16-s3vector | S3 Vectors RAG pipelines, transcript indexing with time-anchored chunks, similarity search, metadata filtering, query expansion |
| 17-mcp | Stateful MCP 2025-11-25 workflows over stdio and Streamable HTTP, including explicit session cleanup |
| 18-xml-yaml | XML and YAML parsing/stringifying |
| 19-html-sanitize | HTML sanitization with configurable allowed tags |
| 20-date | Date parsing, formatting, and timezone conversion |
Fixture-backed examples use the compact, synthetic, CC0 inputs in
examples/fixtures. The exhaustive
examples/catalog.json records each flow's execution
category and composable external-service, credential, local-state, and platform
requirements. Review the requirements and effects
legend before execution. For an
experiment, pass --store-dir an empty disposable directory to isolate
IronFlow's run records; this does not redirect node output or undo remote
mutations. The fixture-backed offline subset runs from an isolated working
directory in CI.
Routine work uses focused tests for the behavior changed. Rust changes also
require formatting, the module-size policy, and
cargo clippy --all-targets -- -D warnings; the whole suite is intentionally
not run on each save or ordinary commit.
Enable the repository hooks once per checkout:
git config --local core.hooksPath .githooksBefore a develop push, the pre-push hook fails closed unless the worktree is
clean, no open pull request targets develop, remote develop is integrated,
and the committed version is a new X.Y.Z-dev.N. It then runs the full local
integration gate, including disposable Redis and PostgreSQL tests. Start or
advance a candidate with:
bun run scripts/development_version.ts bump minor # 1.15.0 -> 1.16.0-dev.1
bun run scripts/development_version.ts bump next # 1.16.0-dev.1 -> dev.2CI runs the full suite on pushes to develop and main, with optional manual
dispatch. Its Linux release build is passed directly to Lua example validation;
the example job does not wait for macOS or compile a second release binary.
Default Clippy/tests and combined PostgreSQL/Redis feature checks each share a
single Linux workspace, avoiding isolated check and per-backend compilations.
Container publication uses a version- and digest-pinned Rust/cargo-chef builder
so source and package-version changes retain the dependency layer, backed by a
dedicated zstd-compressed GHCR BuildKit cache manifest. The mutable
buildcache-amd64 tag is build input only; deploy the commit-tagged application
image by its immutable digest. Release promotion creates
release/X.Y.Z from verified develop, finalizes the candidate there with
bun run scripts/development_version.ts finalize, and merges that exact
candidate into main before the stable tag. Stable versions never land on
develop.
The maintained Now / Next / Later roadmap separates
committed work from candidates and records IronFlow's enterprise deployment
boundaries. Bounded operator metrics and the streamed S3-compatible artifact
lifecycle are part of the delivered baseline. A read-only Web UI for flow and
run visualization remains a later candidate rather than an implementation
commitment.
MIT — see LICENSE.