Skip to content

Handler context and embedded fields

A write handler has two boundaries to keep explicit: what belongs to the handler’s transaction, and what belongs to shared infrastructure. Embedded fields add a third boundary in the UI: structured values must never be silently coerced into text.

Shared code that records a metric should not bind to whichever feature happened to call it. Declare the metric in the library-owning feature and use ctx.metricsFor at the call site:

const metrics = ctx.metricsFor("ai-foundation");
metrics.inc("provider_call_total", { provider: "acme" });

The explicit feature name gives the metric one stable kumiko_<feature>_<metric> name across all consumers. Feature names may use the same kebab-case used by defineFeature; the framework normalises the name for metric registration and lookup. An unregistered target is a no-op, which keeps an error-path counter from turning a handled error into a new exception. Other failures — invalid names or metric-type mismatches — still surface.

ctx.db is tenant-scoped and bound to the current write transaction. A failed handler rolls back writes made through it, including nested writes made through the dispatcher.

ctx.dbOutsideTransaction is also tenant-scoped, but uses the unbound pool. Writes through it survive a rollback of the handler transaction. Use it only to record a fact whose external side effect has already happened and must not be forgotten when the business write fails — for example, a provider charge acknowledgement or a delivery-attempt record. It is not a replacement for ctx.db and does not make an otherwise atomic workflow atomic.

Model repeated structure as an embedded list

Section titled “Model repeated structure as an embedded list”

Use createEmbeddedListField when rows have no identity, history, or independent lifecycle. derived cells describe values such as amount = quantity × unitPrice; the server recomputes them before validation and overwrites any client-supplied value. Missing source values leave an incomplete multiplication unset rather than inventing zero. The client widget uses the same arithmetic helper for live display, but the server is the authority on every write.

The runnable embedded sample shows invoice lines with reference, select, money, totals, and embeddedListDerived values. Use an aggregate instead when a row needs its own status, history, or handler.

Treat unsupported structural fields as read-only

Section titled “Treat unsupported structural fields as read-only”

An embedded field without an embedded-list widget, a jsonb field, and a multiSelect field carry objects or arrays. If no dedicated editor exists, the renderer shows an informational, read-only Banner. It does not fall back to an editable text input: coercing an object or array to text can turn a value into "[object Object]" or "a,b" and overwrite the real data on save.

embeddedListCells carries per-cell metadata for the list editor, and embeddedListDerived carries derived-cell definitions. For structural fields without a dedicated editor, keep the value read-only rather than adding a text fallback.