Skip to content

Event Schemas

A dogwood policy set is checked against two schemas, and only one of them is required.

HalfFormatFlagRequired
Action schemaCedar .cedarschema — entities, actions, each action’s context--policy-schemaYes, for validate, lower and replay
Service schema.dwschema event DSL, a providers.json, a .dw macro library--event-schema, --providers, --macrosNo

The action schema is the one the rest of this lexicon already generates from — see Schema. This page is about the other half.

With all three service flags omitted, upstream falls back to ServiceSchema::defaults(): request (deciding), response and error kinds, a universal pin callerPrincipal = principal, a 24h max_window cap, no providers, and the embedded default macro library.

The grammar is 136 lines of pest and purely syntactic: an optional max_window directive, then a sequence of event declarations.

max_window = 30d
decision event <A>::request {
...inputs(A),
pin callerPrincipal: principalType(A) = principal,
callerResource: resourceType(A),
requestId: String,
}

A is a symbolic action binder, not an action. The file names no actions at all — it says what shape an event of each kind has, for whichever action it is derived against. That is why DWDC010 can check a predicate’s event kind against the emitted schema but not its action: the action half of that check lives in the .cedarschema, and it is the CLI’s to make.

Event kind names are author-defined. request, response and error are conventional, not fixed.

BuilderEmits
spreadInputs() / spreadOutputs()...inputs(A) / ...outputs(A)
field("requestId", concrete("String"))requestId: String
field("callerResource", resourceType())callerResource: resourceType(A)
field("meta", record([…]))a nested record, addressed as meta.member
pinnedField(name, type, pinPrincipal())pin name: … = principal
pinnedField(name, type, pinContext("input.user"))pin name: … = context.input.user

A pinned field must be a leaf; upstream requires the pin prefix and the = … clause together, and the builder enforces both rather than deferring to the parser.

import { TemporalEventSchema, dogwood } from "@intentius/chant-lexicon-cedar";
export const events = new TemporalEventSchema({ schema: dogwood.defaultEventSchema() });

That reproduces upstream’s pinned.dwschema — the shape ServiceSchema::defaults() uses — and emits events.dwschema:

// The default event-schema shape: request/response/error, each correlated to
// the deciding request's principal.
decision event <A>::request {
...inputs(A),
pin callerPrincipal: principalType(A) = principal,
callerResource: resourceType(A),
requestId: String,
sessionId: String,
}
event <A>::response {
...inputs(A),
...outputs(A),
pin callerPrincipal: principalType(A) = principal,
callerResource: resourceType(A),
requestId: String,
sessionId: String,
}
event <A>::error {
...inputs(A),
pin callerPrincipal: principalType(A) = principal,
callerResource: resourceType(A),
requestId: String,
sessionId: String,
}

The pin is the thing to understand before writing your own schema. pin callerPrincipal = principal correlates every temporal predicate to the deciding request’s principal: events logged by other principals are invisible to formerly, since and every aggregate over them.

Supplying any event schema opts out of upstream’s default wholesale. So a schema emitted without a pin does not merely fail to add a correlation — it removes one the policy author very likely assumed, and every predicate in the set starts matching other principals’ events.

That is a legitimate design; cross-principal correlation is a reason to write your own schema. It is also a decision, so chant makes it a named argument:

export const events = new TemporalEventSchema({
schema: dogwood.defaultEventSchema({ pinCallerPrincipal: false }),
});

which stamps the reasoning into the emitted file as a comment, and which DWDS010 reports as a warning in the build. Neither stops you. Both make the choice visible in a diff.

The directive caps how far back any operator in the set may look. Absent, the cap is upstream’s 24h default — the same 24h that applies when no schema is supplied at all.

dogwood.defaultEventSchema({ maxWindow: "30d" }); // max_window = 30d

DWDC011 does the arithmetic in TypeScript and fails the build on a window past the cap, with no binary involved. Where several schemas are emitted the tightest cap wins, and macro-call intervals count: once(48h, …) expands through within ?w and looks back exactly as far as formerly within 48h.

One .dwschema per file, because max_window is a single directive at the top and concatenating two schemas would emit something upstream rejects. A build with more than one gives each an explicit filename:

export const gateway = new TemporalEventSchema({
schema: dogwood.defaultEventSchema({ maxWindow: "30d" }),
filename: "gateway.dwschema",
});

Two schemas targeting one filename is a serializer warning and only the first is written — a silent merge would produce a file that parses as neither.

The third service flag, --providers, takes a providers.json whose entries carry argumentTypes, an outputType and an implementation. chant has no typed builder for it today; the CLI adapter’s bundle type accepts provider text if you assemble it, and the build’s planner does not emit one.

One upstream trap worth recording even so: the CLI reads --providers as raw text and never resolves scriptFile. Rhai has to be inlined under implementation.script, or replay fails per-evaluation with “rhai implementation has no script” while validate and lower still pass.

  • Validation — DWDC010, DWDC011 and DWDS010 in full
  • Replay — where the events these schemas describe actually come from