The developer API
The developer-facing API is deliberately small. Because the kernel supplies phase, participant, budget, actor, access, and correlation automatically, the API only has to accept the things the platform cannot infer: a message, a level, and optional classification. There is no buffer to flush, no builder to construct, and no delivery mode to pick — those are the platform’s job.
The surface
Section titled “The surface”Logging lives on the ambient log object available in the effectful tier — inside record-triggered automation, calc-function diagnostics, background jobs, and integration handlers. One method per level:
log.error(message, fields?)log.warn(message, fields?)log.info(message, fields?)log.debug(message, fields?)log.trace(message, fields?)The minimal call is one line, and it is already fully contextual:
log.info('Reached deep-discount branch');// Persisted with: level=INFO, message, phase=effect, participantKey,// correlationId, actor, tenant, budgetAtEntry — none of which the developer typed.Structured fields are an optional typed object, not string concatenation, so they stay machine-queryable:
log.warn('Discount below margin floor', { invoiceId: record.id, discountedTotal: record.discounted_total, marginFloor: record.cost * 1.10,});Logging an error attaches the error envelope automatically when one is in scope:
log.error('Pricing engine refused the line', { lineId: line.id });// If this runs while handling a computation error, the entry carries the// error envelope (code, class, origin) with no extra call.Scenarios
Section titled “Scenarios”A scenario names the unit of work a log belongs to — invoice-recalc, nightly-gp-sync, order-import — so related transactions across time can be filtered as a set. It is a first-class field on the Log, not a tag.
The scenario is defaulted from the entry point and rarely set by hand: a save from the Invoice recalc path defaults to invoice-recalc, a scheduled job to its job key, an API request to its route. Code overrides it only when a finer name is useful:
log.setScenario('invoice-recalc.bulk-reprice');Scenarios are also the natural key for per-scenario configuration — a level threshold or a retention window that differs from the tenant default. A noisy bulk-reprice path can log at WARN and up while an interactive save logs at INFO, without touching code, because the threshold is configuration data keyed by scenario. This is the useful core of Nebula’s LogScenarioRule__mdt, kept.
We do not adopt Nebula’s option to require a scenario before any logging is allowed. Requiring a scenario to log is a guardrail against Apex code that logs with no organizing context; CAOS entries are never context-free (they always carry phase, participant, and correlation), so making a developer name a scenario before they may log adds friction without adding signal.
A tag is a typed, cross-cutting label — pricing, integration, security-relevant — for classification that cuts across scenarios and objects. Tags are added inline:
log.info('External price fetched', { vendor }).tag('integration', 'pricing');Tags are a closed, deploy-managed vocabulary by default — a tag is a metadata-declared value, so it is spell-checked at author time, governed, and reportable, rather than a free-string that fragments into pricing, Pricing, and price across a codebase. Where an org genuinely needs open-ended tagging, tags may be promoted to their own object with a junction; that is a retention/reuse decision, not the default. This keeps Nebula’s rule-based tagging idea — tags applied automatically by a declared rule — as configuration, while dropping the free-string default that makes tag data messy.
Structured fields over string interpolation
Section titled “Structured fields over string interpolation”The fields argument is the deliberate replacement for building a message string out of concatenated values. A structured field is typed, queryable, and safe to mask; a value baked into a message string is none of those. The message is for a human reading the entry; the fields are for a query filtering thousands of them.
// Prefer this — queryable, maskable, typed:log.info('Invoice priced', { invoiceId, total, marginPct });
// Over this — a string a query cannot filter on:log.info(`Invoice ${invoiceId} priced at ${total} (${marginPct}% margin)`);Sensitive data
Section titled “Sensitive data”Because fields are structured, a field can be masked by a declared rule before the entry is persisted — a value matching a sensitivity classification is redacted at write time, not after. This is Nebula’s LogEntryDataMaskRule__mdt idea, kept, but expressed against CAOS’s data classification rather than a bespoke regex list, so masking is consistent with the platform’s existing notion of sensitive fields.
Where you can log, and where you can’t
Section titled “Where you can log, and where you can’t”Logging is an effectful-tier capability, so it obeys the tier boundary:
- Allowed in ADJUST (before-write automation), EFFECTS (after-write automation), POST-COMMIT, background jobs, integration handlers, and calc-function diagnostic output.
- Not allowed as a side effect inside a pure SHAPE or VALIDATE participant — a formula, roll-up, or validation rule may not log, because pure participants must be side-effect-free and statically analyzable. A failed validation still produces a log entry, but the kernel emits it from the envelope; the rule body does not call
log.
This is stricter than Nebula, which lets any Apex log anywhere, and the strictness is intentional: it preserves the property that pure logic is replayable and deterministic.
The comparison, concretely
Section titled “The comparison, concretely”The same intent, in both systems:
// Nebula Logger (Apex)Logger.info('Invoice priced') .setRecord(invoice.Id) .setField(LogEntryEvent__e.Margin__c, marginPct) .addTag('pricing');Logger.saveLog(); // must not be forgotten, or nothing is saved// CAOSlog.info('Invoice priced', { marginPct }).tag('pricing');// related record, phase, participant, budget, correlation, and persistence: automaticThe CAOS call is shorter and carries more durable context, because the parts Nebula makes explicit — the record link is often the triggering row, the persistence call, the transport choice — are things the kernel already knows or already does.
Sources
Section titled “Sources”- Nebula Logger — project README (jongpie/NebulaLogger) — the fluent API (
Logger.info().setRecord().setField().addTag()), the mandatoryLogger.saveLog(), and the seven logging levels. - Nebula Logger — v4.13.3: Optionally Enforce Scenario-Based Logging (discussion #649) —
Logger.setScenario()and the option to require a scenario before logging. - Nebula Logger — LogEntryDataMaskRule__mdt (data masking) & LogEntryTagRule__mdt (rule-based tagging) — the declarative masking and tagging metadata types kept here as configuration.