Skip to content

Function Registration History

Every function update, status change, rollback, and deletion is recorded as an immutable entity event on the ironflow:fn:{id} stream. This gives you a full audit trail of configuration changes with per-version snapshots, actor attribution, and change reasons.

Configuration history is not executable history

Section titled “Configuration history is not executable history”

Snapshots store function configuration, not handler source or build artifacts. Registration compares configuration and skips an unchanged registration, so a code-only redeploy can retain the version. Rollback restores settings as a new history entry; it does not restore handler code or migrate persisted step results. Event upcasters apply to event schemas, not those results.

A run records the function’s history entity version at creation. Pull worker capabilities derive their version from the live function record, and push dispatch resolves live configuration. Neither mechanism guarantees an immutable executable for a run. See workflow deployment precautions for the version distinctions, compatibility risks, and current safe deployment choices.

Client
│ RegisterFunction(id, config, change_reason)
Handler.RegisterFunction
├─[new]──► store.CreateFunction(fn, snapshot) ← single transaction
│ ├─ INSERT INTO functions (version=1)
│ └─ INSERT INTO events
│ entity_id = "ironflow:fn:{id}"
│ entity_type = "function"
│ entity_version = 1
│ name = "FunctionCreated"
│ data = { snapshot of initial config, change_type = "created" }
└─[update]─► snapshot post-update config (state AT new version)
store.UpdateFunction(fn, snapshot) ← single transaction
├─ SELECT MAX(entity_version)
│ FROM events
│ WHERE entity_id = "ironflow:fn:{id}"
├─ INSERT INTO events
│ entity_id = "ironflow:fn:{id}"
│ entity_type = "function"
│ entity_version = prev + 1
│ name = "FunctionUpdated"
│ data = { snapshot of new config }
└─ UPDATE functions SET ... WHERE id = ?
engine.NotifyFunctionUpdated(ctx, fn)
engine.SyncFunction(ctx, fn)

Same transactional path as a config update, with change_type = "status_change".

DeleteFunction(id)
├─ 1. GetFunction(id) ← capture final state
├─ 2. store.UpdateFunction(fn, snapshot)
│ name = "FunctionDeleted"
│ change_type = "delete"
│ └─► entity event persists after row is gone
└─ 3. store.DeleteFunction(id) ← row deleted, event survives

The deletion snapshot is written before the row is removed so the final configuration remains queryable through ListFunctionHistory even after the function no longer exists.


ListFunctionHistory(fn_id, limit, from_version)
store.ListEntityEvents(
entity_id = "ironflow:fn:{fn_id}",
direction = "backward", ← newest first
BeforeVersion = from_version, ← exclusive backward cursor (proto field `from_version` maps here)
limit = limit + 1 ← +1 to detect has_more
)
events table rows
└─► for each row: json.Unmarshal(data) → FunctionConfigSnapshot
[]FunctionHistoryEntry
{ entity_version, snapshot, actor_id, change_reason, change_type }
has_more = len(results) > limit

Entries are returned newest-first. The proto field from_version maps to opts.BeforeVersion (an exclusive backward cursor). Pass the lowest entity_version from the previous page to fetch the next.

GetFunctionAtVersion(fn_id, version)
store.ListEntityEvents(
entity_id = "ironflow:fn:{fn_id}",
FromVersion = version, ← point lookup at this exact version
limit = 1,
direction = "forward"
)
├─[not found or version mismatch]──► CodeNotFound
└─[found]──► FunctionHistoryEntry

RollbackFunction(fn_id, target_version, change_reason)
├─ 1. GetFunction(fn_id)
│ currentFn ← current live config
├─ 2. GetFunctionAtVersion(target_version)
│ snapshot.Function ← old config to restore
│ └─ guard: reject if snapshot.Status == archived
├─ 3. Reconstruct fn from snapshot
│ fn.ID = currentFn.ID
│ fn.Version = currentFn.Version + 1
│ fn.EnvironmentID = currentFn.EnvironmentID
├─ 4. Build rollback snapshot (state AT new version = restored config)
│ {
│ Function: fn, ← restored config (state at new version)
│ ChangeType: "rollback",
│ ChangeReason: "rollback to v{N}"
│ }
├─ 5. store.UpdateFunctionVersioned(fn, currentFn.Version, snapshot)
│ CAS: WHERE id = ? AND version = ?
│ ├─[version conflict]──► CodeAborted (retry)
│ └─[ok]──► same atomic transaction as write flow
│ entity event written, function row updated
├─ 6. engine.SyncFunction(ctx, fn)
└─ 7. engine.NotifyFunctionUpdated(ctx, fn)

Rollback is itself recorded as a new history entry, so the audit trail shows both the intent and the resulting config change.


History events are rows in the shared events table, namespaced under the reserved ironflow: entity ID prefix. User-defined entity streams cannot use this prefix.

events table
┌──────────────────┬──────────────┬────────────────┬──────────────────────┐
│ entity_id │ entity_type │ entity_version │ name │
├──────────────────┼──────────────┼────────────────┼──────────────────────┤
│ ironflow:fn:abc │ function │ 1 │ FunctionCreated │
│ ironflow:fn:abc │ function │ 2 │ FunctionUpdated │
│ ironflow:fn:abc │ function │ 3 │ FunctionDeleted │
│ user:order:xyz │ order │ 1 │ OrderPlaced │ ← user stream
└──────────────────┴──────────────┴────────────────┴──────────────────────┘

Each data column is a FunctionConfigSnapshot JSON blob containing:

FieldDescription
functionFull function config as of this version (state AT this entity_version, not before it)
actor_idAPI key or user that triggered the change
change_reasonOptional free-text label (e.g. "deploy v2.1.0")
change_typecreated | update | status_change | rollback | delete
snapshot_version0 (absent) = legacy pre-change semantics; 1 = current post-change semantics

RPCDescription
RegisterFunctionPass change_reason to annotate the history entry
ListFunctionHistoryPaginated history, newest-first, keyset via from_version (maps to BeforeVersion). limit defaults to 50, capped at 200.
GetFunctionAtVersionFetch snapshot at a specific entity_version
RollbackFunctionRestore a prior version; recorded as a rollback entry