Skip to content

Capture & the save order

The central claim of CAOS logging is that context is captured by construction, not by hand. A logging framework bolted onto a platform can only record what the developer thinks to attach; a logging layer built into the kernel records what the kernel already knows. This page describes exactly what the kernel knows, where in the save order it knows it, and how that becomes structured log context with no developer effort.

The principle: the runner already has the context

Section titled “The principle: the runner already has the context”

When a developer writes a log line, three kinds of information matter: the message (what the code wanted to say), the context (where in the system this happened, to whom, at what cost), and the correlation (what else belongs to the same unit of work). On a bolt-on logger the developer must assemble all three. In CAOS the developer supplies only the message; the kernel supplies context and correlation because it is the component executing the save and it already tracks them for its own purposes.

Context dimension Who knows it How it’s captured today
Which save phase is running The pipeline, via its phase-entry trace Already recorded as an ordered trace with a per-phase run count
Which participant raised the line The pipeline, which invokes each participant by key Known at dispatch; not yet stamped onto anything durable
Budget spent so far The save governor’s running counters (fuel, DML, rows, outward calls, value-cells) Tracked continuously; a breach already produces a limit error
The caller and their access The access resolvers that gate every read and write Resolved on every operation; the same resolver narrows what the log reader may later see
The correlation id Nobody, yet — it is minted only on the failure path and not threaded The one missing primitive (below)

The design does not add measurement. It persists measurement the kernel already performs, and stamps it onto a log entry at the moment the entry is created.

The missing primitive: one correlation id, threaded end to end

Section titled “The missing primitive: one correlation id, threaded end to end”

Today a correlation id is minted only when the error normalizer hits its internal catch-all, and it lives only long enough to key a telemetry write. There is no single id that is born with the request, flows through every save phase, rides every outbox message, stamps every error envelope, and lands in telemetry. Introducing that id is the foundational change, because every correlation guarantee — the audit surface’s “walk one failure across all three streams”, a Log record gathering all its entries, an error linking to the trace that raised it — is unenforceable without it.

The id is minted once, at the entry boundary of a unit of work: the start of a save request, an API request, a background job invocation, or a deploy. From there it is carried on the ambient transaction context and stamped, without further developer action, onto:

  • every phase-trace entry as the save proceeds,
  • every log entry the developer or the kernel creates,
  • every outbox message enqueued during the effects phase,
  • every error envelope the normalizer emits (not just the catch-all path), and
  • the telemetry record for any raw detail.

Capture is not a new phase. It is a set of observations at the existing phase boundaries the save order already defines. Nothing here changes the seven-phase sequence; logging rides alongside it.

Save phase What is captured Entry-level context stamped
Request entry Mint/inherit the correlation id; open an in-memory Log for this transaction correlationId, actor, tenant, scenario, startedAt
SHAPE Any developer log.* from a derivation; calc-function diagnostics phase: shape, participantKey, budget-so-far
ADJUST Developer logs from before-write automation phase: adjust, participantKey, the triggering record
VALIDATE A failed rule becomes a WARN/ERROR entry carrying its error envelope phase: validate, errorEnvelope, fieldApi
WRITE Constraint/RLS/lifecycle-gate outcomes; roll-up maintenance notes phase: write, the persisted record id
EFFECTS Developer logs from after-write automation; each entry’s log line is staged into the outbox alongside the effect’s own outbound messages phase: effect, participantKey, budget-so-far
COMMIT The transaction outcome is stamped onto the Log: committed, durationMs, final budget snapshot, phaseRuns closes the Log envelope
POST-COMMIT Buffered entries are published as durable Log/Log Entry records (see delivery) —

Two facts about this table are load-bearing:

  1. Every entry inherits its phase and participant automatically. A log.info('reached pricing branch') written inside an after-write effect is stamped phase: effect and tagged with the effect’s participant key without the developer naming either. On Nebula the equivalent requires the developer to know and attach that context by hand, and most never do.
  2. The budget snapshot is free. Because the governor counts continuously, budgetAtEntry on each entry and the final budget on the Log are reads of a counter the kernel maintains anyway — you get “this transaction spent 84 of 150 DML and 6.2s of 10s CPU” with no instrumentation.

A Log is built up in memory across the transaction and sealed at the outcome boundary:

  • Entries accumulate against the ambient Log as log.* calls fire in each phase.
  • At COMMIT, the kernel seals the envelope: outcome committed, duration, the final budget snapshot, phaseRuns (proving the single-pass property — each phase ran at most once), the highest level seen, and the entry count.
  • If the transaction aborts, the seal records rolled_back and the reason, and delivery switches to the out-of-band path so the failure is not lost with the rows.

Because the envelope is sealed from data the kernel already holds, a Log is a faithful, structured record of the transaction — not a best-effort reconstruction from scattered System.debug strings.

Automatic context does not mean the developer is mute. The developer API lets code add exactly the things the kernel cannot infer:

  • the message and its level,
  • a scenario naming the unit of work (defaulted per entry point, overridable),
  • tags for cross-cutting classification,
  • a related record when the meaningful subject differs from the triggering row, and
  • structured fields — arbitrary typed key/values for machine-readable detail.

Everything else — phase, participant, budget, actor, access, correlation — is supplied by the kernel.

How Nebula Logger does it, and why the kernel wins here

Section titled “How Nebula Logger does it, and why the kernel wins here”

Nebula’s LogEntryEventBuilder exposes a fluent surface — .setRecord(), .setField(), .addTag(), .setExceptionDetails() — precisely because Apex code has no ambient access to the transaction’s execution context. Apex cannot ask “which order-of-execution step am I in?” or “how much of my DML limit have I spent, attributed to this line?”; the platform does not expose per-step attribution to user code. So Nebula’s developer must manually attach every piece of context they want, and the framework enriches what it can from Limits and UserInfo at save time. This is not a Nebula flaw — it is the ceiling of what an app-layer framework on a closed platform can reach.

CAOS is on the other side of that boundary: it is the runner. The phase, the participant, the budget, and the access checks are the kernel’s own working state, so attaching them to a log entry is a field copy, not an API the developer must remember to call. That is the entire reason CAOS logging can be more succinct and richer than Nebula at the same time — less to type, more captured.