Skip to content

Ironflow v0.34.0

Ironflow v0.34.0 makes the event-schema registry real. A JSON Schema registered for an event name is now enforced on every server emit path, opt-in through one environment variable, and a Zod schema on createFunction validates payloads at runtime instead of at compile time only. The release also closes a four-phase internal store review, which produced a cross-environment worker hijack fix, PG/SQLite sentinel parity, and an outbox backoff that no longer breaks its own deduplication guarantee.

  • JS SDK: a schema on createFunction now validates at runtime. The Zod schema was compile-time only. Every execution path — serve() push, createWorker() REST pull, createStreamingWorker(), and the @ironflow/node test harness — now parses event.data with it before the handler runs. A match hands the handler the parsed value; a mismatch fails the run with a non-retryable SchemaValidationError (VALIDATION_ERROR), capped at 10 issues. Async refinements are supported. Cron ticks are exempt, because their payload is engine-fabricated. Migration: confirm each declared schema matches what your producers actually emit before upgrading, or remove the schema field from functions whose payloads have drifted (#1948, #1952)
  • @ironflow/core no longer exports mapRestRunResponse. It existed for four days as an incidental export of a REST decoding workaround, which the move of resumeRun onto ConnectRPC removed. Migration: use the shared run mapper the other run APIs already share; no application code should have imported this (#1963, #1978)
  • The event-schema registry is enforced on the emit path. The registry has stored a JSON Schema per (event name, version, environment) since its first migration, and nothing read it. Helper.Emit, EmitEntityEvent, EventRouter.processEvent, and the NATS ingest path now validate the payload against the registered schema. Enforcement is opt-in via IRONFLOW_EVENT_SCHEMA_ENFORCEMENT (off, warn, reject), default off, so a deployment that registered nothing pays no synchronous registry read. Guards sit after the idempotency early-return and before any write, so a rejection costs zero rows and zero NATS traffic. Engine-generated events (cron, function.invoked) are exempt. A registry infrastructure error fails closed rather than passing the event through, the mode is validated at boot, and an absent payload normalizes instead of bypassing validation. A new EventSchemaService/CheckEnforcement RPC reports, per registered schema, whether enforcement is active for it, so you can see what the current mode covers before you switch it on (#1951, #1958, #1960, #1986, #1987, #1993, #1994)
  • Every emit path can select an event schema version. Producers pin the schema version they wrote against instead of always resolving the latest registered one (#1955, #1985)
  • ListRuns supports cursor pagination end to end. The cursor reaches the store, the RPC, and the SDKs, so large run histories page without offset scans (#1962, #1975)
  • Four MCP list tools gain offset paging. Agents reading through the MCP server can walk past the first page of results (#1973, #1988)
  • A worker could flip another environment’s function into pull mode. UpdateFunctionMode ran WHERE id = ? with no environment filter, and its only production caller passed client-supplied function ids straight from the worker register body. A worker registering under environment B could name environment A’s function id: A’s push dispatch stopped and its runs waited for a pull worker that never polled. The store now applies the environment filter on both backends, and RegisterWorker drops a rejected id from the worker’s served set instead of indexing the worker under another tenant’s function. A transient error is not treated as a rejection (#1933, #1945, #1996)
  • User reads and writes are scoped to the caller’s organization, alongside audit-log bound checks, a capacity sweep correction, a circuit-breaker probe fix, and an entity-version race in the same review pass (#1996)
  • A worker whose session was reaped heartbeated forever with zero capacity. RefreshWorkerSessionHeartbeat swallowed ErrNotFound as benign, and no path re-provisioned a session. The heartbeat now mints a fresh session, and keeps the reaped id when that attempt fails so the next heartbeat retries (#1996)
  • SQLite duplicate-row errors now map onto the store sentinels. Postgres mapped a unique violation to ErrAlreadyExists at five insert sites and the SQLite twin returned the raw driver error at all five, while callers branch on the sentinel. On SQLite, every JetStream redelivery of an already-stored event was NAKed to MaxDeliver and dead-lettered, so the crash-mid-dispatch recovery described in ADR 0007 never ran. CreateEvent, CreateEntityEvent, CreateStep, and CreateWaitingCorrelation are fixed, and an entity optimistic-concurrency collision is no longer collapsed onto the duplicate sentinel (#1961)
  • Outbox retries no longer publish duplicate NATS messages. MaxBackoff was 10 minutes against a 2-minute JetStream duplicate window, so attempts 8, 9, and 10 re-published a message id after the window closed and JetStream assigned a second sequence. The default caps at 60s. MaxAttempts stays at 10, so time-to-dead-letter during a NATS outage drops from about 17 minutes to about 5; DLQ rows are recoverable with ironflow outbox dlq, a duplicate sequence is not (#1970, #1977)
  • Projection boolean filters mean the same thing on both backends. A BOOLEAN column in a SQLite projection table holds text, because the SQL projection handler writes flattened string values and BOOLEAN carries numeric affinity. Binding a Go bool matched nothing (#1965, #1977)
  • A projection rebuild could return 500 for the whole /metrics scrape. The rebuild gauge was keyed by projection name and environment name, which is unique only per project, so two projects rebuilding the same projection in like-named environments emitted identical label tuples and Registry.Gather failed. The gauge is now keyed on the environment ID, which projection_registry guarantees unique; the readable name stays as a second label, so existing by (name, env) aggregations still work (#1921, #1931)
  • Migrations 042 and 048 no longer lock the events table for a full backfill. Each batched ADD COLUMN, backfill, and index build into one transaction, and ADD COLUMN holds ACCESS EXCLUSIVE until commit, so a rolling upgrade blocked every read and write on the table for the duration. Each is split into column, backfill, and concurrent index steps. An already-migrated database is unaffected and the resulting schema is byte-identical (#1953)
  • A deduplicated resume returns 409 instead of 500. ErrResumeDeduplicated had no case on the ConnectRPC handler and fell through to CodeInternal. The Go SDK marks 5xx retryable, so a deduplicated resume told callers to retry the exact thing the error exists to prevent. It now maps to CodeAlreadyExists, matching what REST always returned, and both SDKs expose a typed conflict error (ErrConflict in Go, ConflictError in JavaScript) (#1963, #1978)
  • Non-object run inputs and outputs are no longer silently dropped. About 40 conversion sites ran payloads through a helper that unmarshals into a map, discarded the error, and left the field nil, so a function returning [1,2,3] handed its caller undefined with no error and no log line (#1963, #1979)
  • The Go and JS SDK run APIs decoded the wrong protobuf JSON shape (#1919, #1964)
  • TriggerSync and emitSync handle run status correctly across the Go, Node, and Browser SDKs. Wait expiry is separate from run status, sync invoke is function-keyed, and the fan-out contract is explicit (#1918, #1920, #1929, #1950)
  • ListAPIKeys returns a stable order, which was the root cause of an intermittent test failure (#1980)
  • Request-body schema requiredness is gated. schemagen inferred requiredness from Go serialization tags, so a field without omitempty was published as required. That reached sdk-manifest.json, both OpenAPI copies, the Python TypedDicts, and every Tier-2 client. A test now flags a multi-field request body where every field is required, with five argued exemptions (#1982, #1991)
  • Named request types are decoded rather than mirrored, removing a class of drift between the handler and the published spec (#1983, #1992)
  • The expected_version zero-value trap on entity append is documented (#1984, #1990)
  • onEvent is documented as fire-and-forget, not awaited (#1916)
  • The Python SDK README reaches parity with the JS and Go mirrors (#1915)
  • The last five temporary coverage exclusions are cleared (#1957, #1981)
  • A polyglot order-processing reference app is under construction at examples/reference-app, replacing the 39-route SDK feature gallery (#1894)

Ironflow Desktop ships on its own cadence — see the Desktop changelog for its release notes.