- Releases
- Ironflow v0.34.0
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.
Breaking Changes
Section titled “Breaking Changes”- JS SDK: a
schemaoncreateFunctionnow validates at runtime. The Zod schema was compile-time only. Every execution path —serve()push,createWorker()REST pull,createStreamingWorker(), and the@ironflow/nodetest harness — now parsesevent.datawith it before the handler runs. A match hands the handler the parsed value; a mismatch fails the run with a non-retryableSchemaValidationError(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 theschemafield from functions whose payloads have drifted (#1948, #1952) @ironflow/coreno longer exportsmapRestRunResponse. It existed for four days as an incidental export of a REST decoding workaround, which the move ofresumeRunonto ConnectRPC removed. Migration: use the shared run mapper the other run APIs already share; no application code should have imported this (#1963, #1978)
Features
Section titled “Features”- 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 viaIRONFLOW_EVENT_SCHEMA_ENFORCEMENT(off,warn,reject), defaultoff, 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 newEventSchemaService/CheckEnforcementRPC 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)
ListRunssupports 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.
UpdateFunctionModeranWHERE 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, andRegisterWorkerdrops 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.
RefreshWorkerSessionHeartbeatswallowedErrNotFoundas 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
ErrAlreadyExistsat 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 toMaxDeliverand dead-lettered, so the crash-mid-dispatch recovery described in ADR 0007 never ran.CreateEvent,CreateEntityEvent,CreateStep, andCreateWaitingCorrelationare fixed, and an entity optimistic-concurrency collision is no longer collapsed onto the duplicate sentinel (#1961) - Outbox retries no longer publish duplicate NATS messages.
MaxBackoffwas 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.MaxAttemptsstays at 10, so time-to-dead-letter during a NATS outage drops from about 17 minutes to about 5; DLQ rows are recoverable withironflow outbox dlq, a duplicate sequence is not (#1970, #1977) - Projection boolean filters mean the same thing on both backends. A
BOOLEANcolumn in a SQLite projection table holds text, because the SQL projection handler writes flattened string values andBOOLEANcarries numeric affinity. Binding a Go bool matched nothing (#1965, #1977) - A projection rebuild could return 500 for the whole
/metricsscrape. 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 andRegistry.Gatherfailed. The gauge is now keyed on the environment ID, whichprojection_registryguarantees unique; the readable name stays as a second label, so existingby (name, env)aggregations still work (#1921, #1931) - Migrations 042 and 048 no longer lock the
eventstable for a full backfill. Each batchedADD COLUMN, backfill, and index build into one transaction, andADD COLUMNholdsACCESS EXCLUSIVEuntil 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.
ErrResumeDeduplicatedhad no case on the ConnectRPC handler and fell through toCodeInternal. 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 toCodeAlreadyExists, matching what REST always returned, and both SDKs expose a typed conflict error (ErrConflictin Go,ConflictErrorin 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 callerundefinedwith no error and no log line (#1963, #1979) - The Go and JS SDK run APIs decoded the wrong protobuf JSON shape (#1919, #1964)
TriggerSyncandemitSynchandle 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)ListAPIKeysreturns a stable order, which was the root cause of an intermittent test failure (#1980)
Improvements
Section titled “Improvements”- Request-body schema requiredness is gated.
schemageninferred requiredness from Go serialization tags, so a field withoutomitemptywas published as required. That reachedsdk-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_versionzero-value trap on entity append is documented (#1984, #1990) onEventis 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.