Skip to main content

Events to ratings DSL

This page is the DSL reference for Event -> Rating policies.

Source of truth used for this document:

  • DSL compiler/evaluator: vluna/oss/packages/vluna-core/src/services/event-to-ratings.dsl.ts
  • Runtime execution: vluna/oss/packages/vluna-core/src/features/billing/services/event-to-ratings.service.ts
  • Supplemental design note in repo: docs/instructions/event_to_ratings_dsl.md

1) DSL root shape (v1)

{
"dsl_version": "v1",
"engine": "single",
"params": {},
"match": {
"event_type": "demo.outcome"
},
"emit": {
"intents": []
}
}

Required root fields:

  • dsl_version: must be "v1"
  • engine: "single" | "aggregate"
  • match
  • emit

Required nested fields:

  • match.event_type
  • emit.intents (must be non-empty)

2) Supported syntax summary

2.1 match.event_type

Supported now:

  • string exact form: "job.completed"
  • object exact form: { "op": "exact", "value": "job.completed" }

Not supported by current compiler:

  • { "op": "prefix", ... }
  • { "op": "regex", ... }

Those forms compile with error (not implemented).

2.2 match.where predicate operators

Logical:

  • { "all": [predicate, ...] }
  • { "any": [predicate, ...] }
  • { "not": predicate }

Comparison:

  • { "eq": [lhs, rhs] }
  • { "ne": [lhs, rhs] }
  • { "in": [lhs, [rhs...]] }
  • { "gt": [lhs, rhs] }
  • { "gte": [lhs, rhs] }
  • { "lt": [lhs, rhs] }
  • { "lte": [lhs, rhs] }
  • { "exists": [ref] }
  • { "prefix": [lhs, "text"] }
  • { "contains": [lhs, "text"] }

2.3 Value expressions used by predicates

Supported ref/value forms:

  • literals: null | boolean | number | string
  • { "event": "subject_ref" | "occurred_at" | "billing_account_id" | "event_type" | "semantic_kind" }
  • { "payload": "a.b.c" } (dot path)
  • { "label": "env" }
  • { "param": "param_name" }
  • any string_expr
  • any int_expr

3) emit.intents (rating output)

Each intent emits one internal ingest commit.

Intent shape:

{
"link_kind": "billed",
"feature_code": "svc.job",
"budget_id": { "const": "1001" },
"feature_quantity_minor": { "payload_int": "units", "default": 0 },
"meters": [
{ "meter_code": "svc.job.runtime_ms", "quantity_minor": { "payload_int": "duration_ms", "default": 0 } }
],
"labels": {
"job_id": { "payload_str": "job.id" }
},
"metadata": {
"raw_event_type": { "event_str": "event_type" }
}
}

Constraints enforced by compiler/runtime:

  • link_kind: optional, default billed; allowed billed|adjustment|reversal|shadow
  • feature_code: required slug (^[a-z0-9]+([._-][a-z0-9]+)*$)
  • meters: required non-empty
  • each meters[].meter_code: required slug
  • each meters[].quantity_minor: required int_expr
  • resolved integer quantities are truncated and clamped to >= 0

4) Expressions

4.1 int_expr

Supported forms:

  • literal number (e.g. 12)
  • { "const": 12 }
  • { "payload_int": "path", "default": 0 }
  • { "label_int": "key", "default": 0 }
  • { "param": "name" }
  • { "mul": [int_expr, int_expr] }
  • { "div": [int_expr, int_expr], "rounding": "floor|ceil|nearest" }
  • { "agg": { ... } } (aggregate engine)

agg form

Supported ops:

  • count
  • sum
  • avg
  • min
  • max
  • count_distinct

of rules:

  • count: of must not be provided
  • sum|avg|min|max: of is required, only payload or label is allowed
  • count_distinct: of is required, supports payload|label|event

of.event allowed fields:

  • subject_ref
  • occurred_at
  • billing_account_id
  • event_type
  • semantic_kind

avg supports optional rounding:

  • floor (default)
  • ceil
  • nearest

Note: compiler computes aggregate key internally (count or op:source:field). A user-provided key is not used.

4.2 string_expr

Supported forms:

  • literal string (e.g. "abc")
  • { "const": "abc" }
  • { "payload_str": "path", "default": "fallback" }
  • { "label_str": "key", "default": "fallback" }
  • { "event_str": "subject_ref" | "event_type" | "billing_account_id" | "semantic_kind" }
  • { "param": "name" }

4.3 any_expr (used mainly in metadata)

Supported:

  • any JSON literal
  • nested objects/arrays
  • embedded string_expr / int_expr

5) params and contract-term binding

params defines typed parameters and optional contract source mapping.

Supported param specs:

  • int
  • string
  • string[]
  • bool

Example:

{
"params": {
"min_duration_ms": {
"type": "int",
"min": 0,
"default": 0,
"source": { "term_key": "meeting.success.min_duration_ms" }
},
"allowed_envs": {
"type": "string[]",
"default": ["prod"],
"source": { "term_key": "meeting.success.allowed_envs" }
}
}
}

term_key behavior:

  • normalized by normalizeIdentifier(term_key, 'term_key')
  • must be 1..128 chars
  • allowed charset [a-z0-9._/@:-]+
  • must start/end with [a-z0-9]

Runtime term lookup:

  • reads contract_terms with kind='e2r_param'
  • per key picks latest effective_at <= occurred_at (single) or <= window_start (aggregate)

Resolution priority:

  1. term value (if present and valid)
  2. param default
  3. error

Runtime errors:

  • contract_term_missing
  • contract_term_invalid

6) Type coercion and evaluation semantics

6.1 Numeric coercion (toNumberMaybe)

Accepted as number:

  • number
  • numeric string
  • boolean (true -> 1, false -> 0)

Otherwise => null.

Used in:

  • payload_int, label_int
  • numeric predicates (gt/gte/lt/lte)

6.2 String coercion (toStringMaybe)

Accepted as string:

  • string
  • number -> string
  • boolean -> "true" / "false"

Otherwise => null.

Used in:

  • payload_str, label_str
  • prefix, contains

6.3 Equality semantics

eq/ne/in use deep equality via JSON.stringify comparison.

6.4 Null behavior

  • policy mismatch returns null (no intents)
  • unresolved meter quantity (null) throws runtime error (meter quantity expression resolved to null)
  • final integer quantities are truncated and clamped to non-negative

7) Engine input shapes

7.1 engine = single

Input is event-based (source_kind='event') with:

  • event fields (event_type, semantic_kind, occurred_at, subject_ref, billing_account_id)
  • payload
  • labels map

7.2 engine = aggregate

Input is grouped (source_kind='aggregate') with:

  • event_type
  • aggregation window (window_start, window_end)
  • aggs map populated by worker

8) Runtime policy-selection behavior

Single-event processing:

  • candidate set: active versions effective_at <= event.occurred_at
  • filter to same event_type and engine='single'
  • if multiple policies match => processing row becomes quarantined
  • if none match and missing-term blockers exist => failed with retry
  • if none match => skipped_no_policy

Aggregate processing:

  • candidate set: active versions effective_at <= window_start
  • filter to same event_type and engine='aggregate'
  • only exactly one match is accepted; otherwise group is skipped

Semantic-kind guardrails:

  • Event-to-Ratings processing expects outcome semantics (expectedMeterSemanticKind='outcome' in runtime path).
  • Policy validation also enforces referenced meters to be semantic_kind='outcome'.

9) Full examples

9.1 Single-event policy

{
"dsl_version": "v1",
"engine": "single",
"params": {
"min_duration_ms": {
"type": "int",
"default": 0,
"source": { "term_key": "meeting.success.min_duration_ms" }
}
},
"match": {
"event_type": "meeting.completed",
"where": {
"all": [
{ "eq": [ { "payload": "status" }, "success" ] },
{ "gte": [ { "payload_int": "duration_ms", "default": 0 }, { "param": "min_duration_ms" } ] }
]
}
},
"emit": {
"intents": [
{
"feature_code": "meeting.success",
"meters": [
{ "meter_code": "meeting.success.count", "quantity_minor": 1 },
{ "meter_code": "meeting.success.duration_ms", "quantity_minor": { "payload_int": "duration_ms", "default": 0 } }
],
"labels": {
"meeting_id": { "payload_str": "meeting_id" },
"env": { "label_str": "env", "default": "unknown" }
}
}
]
}
}

9.2 Aggregate policy

{
"dsl_version": "v1",
"engine": "aggregate",
"match": {
"event_type": "meeting.completed"
},
"emit": {
"intents": [
{
"feature_code": "meeting.daily",
"meters": [
{ "meter_code": "meeting.daily.count", "quantity_minor": { "agg": { "op": "count" } } },
{
"meter_code": "meeting.daily.duration_avg_ms",
"quantity_minor": {
"agg": {
"op": "avg",
"of": { "payload": "duration_ms" },
"default": 0,
"rounding": "nearest"
}
}
}
]
}
]
}
}

10) Authoring checklist

Before creating a policy version:

  • use dsl_version = v1
  • choose engine intentionally (single or aggregate)
  • keep match.event_type exact
  • ensure every intent has non-empty meters
  • ensure feature_code/meter_code are valid slugs
  • for params.source.term_key, ensure matching contract_terms(kind='e2r_param') exists or provide defaults
  • validate through policy validate endpoint before activation

11) What is intentionally not supported yet

  • event_type prefix/regex matching
  • custom aggregate-key names in DSL input
  • non-boolean coercion for bool params from term values

12) Minimal templates

12.1 Minimal single-engine policy

{
"dsl_version": "v1",
"engine": "single",
"match": {
"event_type": "demo.outcome"
},
"emit": {
"intents": [
{
"feature_code": "demo.outcome",
"meters": [
{
"meter_code": "demo.outcome.count",
"quantity_minor": 1
}
]
}
]
}
}

12.2 Minimal aggregate-engine policy

{
"dsl_version": "v1",
"engine": "aggregate",
"match": {
"event_type": "demo.outcome"
},
"emit": {
"intents": [
{
"feature_code": "demo.outcome.daily",
"meters": [
{
"meter_code": "demo.outcome.daily.count",
"quantity_minor": {
"agg": { "op": "count" }
}
}
]
}
]
}
}

12.3 Minimal params + contract-terms policy

{
"dsl_version": "v1",
"engine": "single",
"params": {
"min_value": {
"type": "int",
"default": 0,
"source": { "term_key": "demo.min_value" }
}
},
"match": {
"event_type": "demo.outcome",
"where": {
"gte": [
{ "payload_int": "value", "default": 0 },
{ "param": "min_value" }
]
}
},
"emit": {
"intents": [
{
"feature_code": "demo.outcome.filtered",
"meters": [
{
"meter_code": "demo.outcome.filtered.count",
"quantity_minor": 1
}
]
}
]
}
}

13) Common errors and fixes

Error message (real)Why it happensFix
Unsupported dsl.dsl_version: ...dsl_version is not v1Set dsl_version to v1
dsl.engine unsupported: ...engine is not single or aggregateUse only single or aggregate
dsl.match.event_type.op not implemented: ...prefix/regex op usedUse exact match only
dsl.emit.intents must not be emptyNo intents providedAdd at least one intent
dsl.emit.intents[i].feature_code must be a slugInvalid feature_code formatUse lowercase slug with [a-z0-9._-]
dsl.emit.intents[i].meters[j].meter_code must be a slugInvalid meter_code formatUse lowercase slug with [a-z0-9._-]
...unsupported int_expr / ...unsupported string_exprExpression shape not supportedRewrite using documented expression forms
...agg.of is required when op=...Aggregate op requires ofProvide valid of (payload/label/event depending on op)
...agg.of.event is not allowed for op=...sum/avg/min/max with event sourceUse payload or label source for numeric aggregation
contract_term_missing: term_key=... param=...No term value and no defaultAdd contract term or provide default
contract_term_invalid: term_key=... param=... expected=...Term type incompatible with param typeCorrect term value type in contract_terms
meter quantity expression resolved to nullMeter quantity_minor resolves to null at runtimeEnsure expression has valid source or default
feature_missing / meter_missing (validate result)DSL references unknown feature/meterCreate missing feature/meter first
meter_semantic_kind_invalid (validate result)Referenced meter is not outcomeUse outcome semantic meters for E2R
feature_meter_unmapped (validate result)Meter not linked to featureAdd feature-meter mapping

Recommended debugging order:

  1. Compile/validate DSL first.
  2. Confirm feature/meter existence and semantic kind.
  3. Confirm feature-meter mapping.
  4. Confirm contract terms availability for all required params.
  5. Re-check runtime input fields (payload, labels) used by expressions.