Skip to main content
This page is the reference for the format of an object type: what each field type looks like on the wire, the members a field declares, the checks a type must pass, and niadra-expr, the expression language of keys, values, timers, readings and derived fields. The conformance vectors that fix the language’s behavior ship in both SDK repositories, in spec/vectors/niadra-expr.v0.json: the server and both SDKs run the same file.

Field types and how values travel

An event’s fields and an object push carry each value as JSON, in the form its declared type expects: A value of another kind is kept as sent, but an expression over the field cannot read it as its type: a money amount sent as the string "38000.00" is not a number to a reading, a derived field or a timer. A field the type does not declare is kept as a plain field and opens a drift data issue, never a refusal. What an event says about itself (operation, version, last_event_type, source_version, record_ref, input_refs) is bookkeeping: never drift, and served only when the type declares it as a field. Every key of an event’s fields applies to every object in its object_refs, so send one object per event.

What a field declares

A plain field declares no observers and no unobserved_blocks. A change of a field’s sensitivity, access or pii, of a source’s licence or of the type’s purposes is a privacy decision: it also needs the role that answers for security.

What the server refuses in a type

Beyond the shape of the document, a type is refused, with the path of each entry that breaks a rule (for example timers.launch_at.on_fire: transition(scheduled->actve) is not a transition of the lifecycle), when:
  1. A field, a value, a time axis or a derived field takes a name another one already has, or a name the language reserves (below).
  2. A state the lifecycle, a timer or a vocabulary names is not declared; a timers_on names an undeclared axis; a content field, completeness.total_field, a known defect, a union key or a refresh action names an undeclared field or value; prefer_source, use_source, quote() or a vocabulary names an undeclared source; a derived field names an undeclared relation, or one whose type the registry does not declare, or a min or max over a field that type does not have.
  3. A derived axis does not name its rule, or a rule is on an axis that is not derived; a type derived by introspection lost its fingerprint.
  4. A member breaks its own rule: a plain field that declares observers, a claim_max_age over max_age, and the rules of sources, the lifecycle, timers, refetch and purposes that Object types and state describes.
  5. An expression does not parse or does not resolve against the type: conditions (when, active_when, where) must give true or false, and a timer’s due a date or a datetime.

niadra-expr

The language is small, deterministic and total: no loops, no recursion, no I/O and no clock but the one it is given, bounded text and nesting, and a result or one of three errors for every input. The same text gives the same result on the server, in each SDK and in your resolver worker.

Tokens

  • Spaces, tabs and line breaks separate tokens.
  • A name is [a-z_][a-z0-9_]*, at most 64 characters, lowercase ASCII.
  • A number is digits with an optional . and at least one digit after it; there is no sign, a negative number comes from subtraction.
  • A duration is a whole number of at most six digits followed by ms, s, min, h or d (m is not a unit, and 1.5h is written 90min).
  • A string is in single quotes, on one line, with \' and \\ as its only escapes, at most 256 characters.
  • The operators are ==, !=, <, <=, >, >=, +, -, (, ), [, ], , and .; the keywords are and, or, not, in, true, false, none, yes, no, unobserved and known_defect.

Grammar

or binds loosest, then and, not, a comparison, + and - (left to right), and .. Comparisons do not chain: a < b < c is an error. A dotted name such as lead.city is one name with parts; after a group or a call, . reads a field of a quote. The logical words appear only on one side of == or !=.

Values and the four logical values

unobserved and known_defect are unknown. A present value is a boolean, a number, a string, a duration (whole milliseconds), a date, a datetime (to the millisecond) or a list. Not observed is never false: a comparison with an unknown value is unknown, and so is its negation.

Names

A one-part name is, in order: a context name (state, the lifecycle state; derived_status; watch_count, 0 by default; purpose, the read’s purpose, display by default); a field, a computed value or a time axis of the object; a declared absence name; otherwise a field nobody observed, unobserved. A dotted name is a declared input of that path (lead.city), config.<key> (the company’s configuration value, absent when not set) or <field>.completeness. The keywords and state, derived_status, watch_count, purpose and config are reserved. A state is a string: state == 'open', never state == open.

Operators

  • x == yes is true exactly when x carries yes, and likewise for the other logical words; the result is always true or false: available == unobserved asks whether a value was observed.
  • Otherwise, when either side of a comparison, + or - is unknown, the result is unknown (known_defect when either carries it).
  • Equality: two absences are equal when either has no name or both have the same one (none is any absence); an absence never equals a present value; numbers by value, strings by their characters, durations by length, dates and datetimes as instants (a date is the start of its day), lists item by item; any other pair is expr_type.
  • Order (<, <=, >, >=) is false when either side is absent; numbers, strings (by code point), durations, dates and datetimes compare; any other kind is expr_type.
  • in takes a list on its right: true when the left equals an item, unknown when an unknown item could decide, false otherwise.
  • and, or and not take true, false or unknown, with three-valued logic, left to right, and the first operand that decides stops the evaluation: false and x never evaluates x.
  • + and -: number with number; duration with duration; a date or datetime plus or minus a duration gives a datetime; a date plus or minus business days gives a date; two dates or datetimes subtracted give a duration. An absent operand gives an absence.

Functions

The deadlines of a court, a contract or a product are your company’s rules, as declared values: the language only counts simple business days.

Errors and bounds

An expression has at most 1,024 characters, 256 tokens and 16 nested groups; a list literal at most 64 items, a string at most 256 characters, a name at most 64 and a duration at most six digits. A list read from a slot has at most 1,000 items, and a business-day count is at most 1,000.

Next steps

Object types and state

What a type is for, freshness, the read purpose and shared objects.

Typed reads

POST /v1/state/read: fields, values, readings and timers.