RIDDL Language Guide¶
Overview¶
RIDDL (Reactive Interface to Domain Definition Language) is a domain-specific language designed for modeling reactive systems using Domain-Driven Design (DDD) principles. It bridges the gap between business domain experts and software engineers by providing a language that's both expressive for domain modeling and precise enough for implementation. This language was also designed to provide an AI model with sufficient context to generate code accurately from the specification.
Language Design Emphasis¶
RIDDL emphasizes:
- Declarative syntax with natural language readability
- Hierarchical structure with domains, (bounded) contexts, entities, processors, repositories, projectors, adaptors, and other definitions that specify the behavior of a system
- Event-driven and reactive design patterns
- State-based entity modeling with explicit state transitions
- Streaming of data between components from outlet to inlet via connectors
- Clear separation of commands, events, queries and results
- Saga-based coordination of complex atomic processes, possibly distributed
- Comprehensive domain modeling through DDD concepts
- Asynchronous, non-blocking communications (implied)
Language Structure¶
Definitions¶
Everything that has a name and can have metadata is known as a Definition in RIDDL.
Branches¶
Branch definitions are containers that hold other definitions. They form the hierarchical structure of a RIDDL model:
- Root: The implicit top-level container (not explicitly defined)
- Module: A flat, named collection of any top-level definition
- Domain: Contains bounded contexts and domain-wide definitions
- Context: Contains entities, repositories, sagas, and other processors
- Entity: Contains states, handlers, functions, and message types
- Epic: Contains use cases describing user interactions
- Saga: Contains steps for multi-step processes with compensation
Processors¶
A processor is any definition that can receive and process messages, and that may declare ports. Processors are the active components of a RIDDL model.
RIDDL has one unified processor model. Every processor kind — Context, Entity,
Adaptor, Projector, Repository, and the generic processor — may declare
inlets and outlets, and may carry an ascribed streaming shape:
- Entity: Stateful processor with commands, events, states, and handlers. Entities are the primary business objects that maintain state and respond to commands.
- Adaptor: Translates messages between two contexts. Defined with a
direction (
fromorto) relative to a context. - Repository: Handles persistence of data. Contains schemas and handlers for storage operations.
- Projector: Projects events to a repository, often transforming or aggregating data for read models (CQRS pattern). Projectors handle events only.
- Processor: The generic streaming processor, declared with the
processorkeyword and an optionalas <shape>ascription. - Context: A bounded context is itself a processor, so it may hold handlers and ports of its own.
A Saga orchestrates multi-step processes with compensation logic for failure recovery. A Function defines a pure, reusable computation. Neither is a processor — both extend the vital-definition base — so neither takes a version or a copyright. A Saga does nonetheless bear inlets and outlets, being a coordinator with messages to receive and emit; a Function does not.
All processors can contain handlers, functions, types, constants, invariants, ports, and other processor-specific definitions.
Streaming Shapes¶
A processor's shape describes its streaming arity. It is normally derived
from the number of ports the processor declares, and may optionally be
ascribed with as <shape> to state the intent explicitly:
| Shape | Description | Inlets | Outlets | Synonym |
|---|---|---|---|---|
source |
Produces data from external sources | 0 | 1+ | |
sink |
Consumes data to external destinations | 1+ | 0 | |
flow |
Transforms data passing through | 1 | 1 | cascade |
merge |
Combines multiple streams into one | 2+ | 1 | fanin |
split |
Divides one stream into multiple | 1 | 2+ | broadcast, fanout |
router |
Routes messages based on content | 1 | 2+ | |
void |
Declares no ports at all | 0 | 0 |
processor OrderEnricher as flow is {
inlet RawOrders is type OrderEvent
outlet EnrichedOrders is type EnrichedOrderEvent
}
If an ascribed shape contradicts the declared arity, that is an Error. If a processor declares at least one port but ascribes no shape, that is a suppressible StyleWarning nudging you to say what you meant.
Deprecated shape keywords
The dedicated keywords source, sink, flow, merge, split and
router still parse but emit a [deprecated] message. Write
processor <id> as <shape> instead. They are slated for removal in 3.0.
Domain Hierarchy¶
RIDDL models are organized hierarchically:
- Root: The top-level container for the entire system. Roots are not declared; they consist of the top-level definitions in a file. A Root may contain Modules, Domains, Authors, Versions, Copyrights and Comments.
- Module: A named, flat collection of any top-level definition, in any
order, with no hierarchy enforced at its own top level. The internal rules of
each contained definition still apply. Modules are the unit of reuse and of
compiled (
.bast) output. - Domain: A container for the specification of some knowledge domain.
- Context: Bounded context containing related entities and components.
- Entity: Stateful business objects with commands, events, and handlers.
- Repository: Persistent storage. May live in a Context or, when it synthesizes data across several contexts, directly in a Domain.
- Projector: A component that projects events to a repository for later retrieval, possibly transforming the data or merging multiple event streams.
- Saga: The orchestration of a multi-step atomic process with compensating actions to undo the process when errors arise.
- Epic: A collection of use cases showing an expected usage pattern.
- Case: A specific user interaction flow.
Nebula is deprecated
Earlier releases had an anonymous top-level scratch pad called a nebula.
A Module now does everything a nebula did and has a name. An anonymous
whole-file sequence of definitions still parses, but emits one
[deprecated] message and yields a Module with the synthetic id nebula.
Wrap your definitions in module <Name> is { … } instead.
Data Types¶
RIDDL has a rich type system supporting both simple and complex data structures.
Predefined Types:
- Strings:
String,String(min,max)with length constraints - Numbers:
Integer,Natural,Whole,Real,Number,Decimal(w,f) - Boolean:
Boolean - Temporal:
Date,Time,DateTime,TimeStamp,Duration,ZonedDateTime(zone) - Identifiers:
UUID,UserId,Id(entity path) - Other:
URL,URL(scheme),Currency(code),Location,Nothing,Anything - Physical:
Length,Mass,Current,Temperature,Luminosity,Mole
Anything is the dual of Nothing: a type assignment-compatible with every
other type in both directions. A port typed Anything is compatible with any
port it is connected to.
Abstract is deprecated
Abstract was renamed to Anything. The old spelling still parses to the
same node and emits a [deprecated] message.
Pattern Types:
Pattern("regex")— String matching a regular expression
Compound Types:
- Aggregation:
{ field1 is Type1, field2 is Type2 }— Named field collections - Alternation:
one of { TypeA, TypeB, TypeC }— Union/sum types. An alternation must offer a real choice: zero alternatives is an Error, exactly one draws a[deprecated]message, two or more is clean, andone of { ??? }remains the way to say "not decided yet". May also be written with bars:TypeA | TypeB | TypeCis the identical type.prettifyrewrites bars back toone of { ... }, which stays the canonical spelling. Predefined types are not valid alternatives either way. - Enumeration:
any of { Value1, Value2, Value3 }— Enumerated values
Aggregate Use Cases:
The keyword that introduces an aggregate says what kind of thing it is:
type, command, query, event, result, record, graph, table.
- Graph:
graph— models graph-structured data - Table:
table— models tabular data
Collection Types:
- Sequence:
sequence of Typeormany Type - Set:
set of Type - Mapping:
mapping from KeyType to ValueType - Optional:
optional TypeorType?
Special Types:
- Entity Reference:
reference to entity Path - Unique ID:
Id(entity Path)— Type-safe entity identifier
Messages and Records¶
There are exactly four messages: command, query, event and result.
Only a message can be sent, told, yielded, or handled by an on clause.
A record is data, not a message. A record can never be sent or handled. It
types an entity's state and supplies the payload of a morph.
Record is no longer a message
In RIDDL 1.x a record reference was accepted wherever a message reference
was. It no longer is. send, tell, yield and on <ref> accept the four
real messages only; state … of and morph … with accept a record.
Basic Syntax Elements¶
-
Readability Words: Optional words that improve human readability of a model.
- The complete list is:
and,are,as,at,by,for,from,in,is,of,so,that,to. - A synonym for
isis:,=, orare. - These words are accepted by the grammar but not required.
- The complete list is:
-
Definition Structure:
- The
[kind]indicates the kind of definition (domain, context, entity, etc.). - The
[name]is the identifier for this definition, which must be unique amongst its peers. - The "definition contents" depend on the
[kind]of definition. - The metadata section must always come after the closing brace of the definition, not within it.
- The
-
Type References: Always specify the kind of reference.
entity Productcommand CreateCartevent OrderCreatedrecord ProductDatauser Customer
Naming Rules¶
How a path identifier resolves¶
Every reference names its target with a path identifier: one or more
identifiers separated by dots, such as Tax.TaxIn or Outer.Deep.Leaf. A
single name and a dotted path resolve by different rules, so they are worth
taking separately.
A single name searches the whole model¶
One identifier with no dots is looked up in the symbol table — a dictionary of every name defined anywhere in the model. If exactly one definition carries that name, that is the answer, wherever it lives. If more than one does, the reference is an ambiguity error listing the candidates; if none does, it is a not-found error.
This is why a bare name can reach a definition in a sibling context without naming it, and why adding a definition elsewhere in the model can turn a working bare reference into an ambiguous one. To make a reference immune to that, qualify it.
A dotted path anchors, then walks down¶
A path of two or more names is resolved in two phases.
Phase 1 — anchor on the first name, tried in this order:
- The literal name
Rootanchors at the root of the model, which makes the rest of the path absolute. - Otherwise, if the name matches one of the ancestors of the place the reference is written, that ancestor is the anchor. The ancestor closest to the reference wins.
- Otherwise the name is looked up in the symbol table, exactly as a single name is: unique anchors the path, ambiguous is an error, absent is an error.
Steps 1 and 2 are the guaranteed forms — they depend only on where the reference is written, so they cannot be broken by a definition added elsewhere. Step 3 is the convenient one, and carries the same ambiguity risk as any bare name.
Phase 2 — walk down, one name at a time. Each remaining name must be found
among the contents of the definition named before it. There is no scope-popping
after the anchor: Tax.TaxIn means "anchor Tax, then look for TaxIn inside
it", and if Tax has no TaxIn the path fails there, even when a TaxIn
exists elsewhere in the model.
context Tax is {
record TaxIn is { subtotal is Natural }
}
context Billing is {
// `Tax` anchors via the symbol table, then `TaxIn` is
// sought among Tax's own contents.
record Invoice is { amount is Tax.TaxIn }
}
What counts as "inside" depends on the kind¶
For most definitions the contents are just the definitions written inside the braces. Several kinds contribute something else, so that paths follow the model rather than the syntax:
| Anchoring definition | What the next name may match |
|---|---|
| State | its own contents (handlers, invariants) first, then the fields of the record it is of — so a handler shadows a same-named field |
| on-clause | the fields of the message being handled |
| Field, Constant, Type | the members of its type expression |
| Inlet, Outlet | the members of the type it carries |
| Function | its own definitions, plus the fields of a deprecated inline requires/returns aggregation |
| Include | the definitions the included file contributes, as though written in place |
Because a State's own contents are searched before the record's fields, a handler named the same as a field is reached by a path, and the field is not. The nearer declaration wins.
Sibling names must be unique, regardless of kind¶
This rule exists because of how paths resolve. Each step of a dotted path picks a definition out of a container by name alone — the kind is never part of the choice — so two definitions sharing a name in one container would leave the step with nothing to decide on.
Uniqueness among siblings is what makes that step well-defined. It does not make a bare name unambiguous, since a bare name is looked up across the whole model: two definitions of the same name in different containers are perfectly legal, and a bare reference to that name is then an ambiguity error at the point of use rather than at either definition.
Two definitions in the same container may not share a name, even when they are different kinds of thing:
context Catalog is {
type Thing is String
entity Thing is { ??? } // Error: 'Thing' is already taken
}
Changed in RIDDL 2.0
Uniqueness used to be checked per kind, so type Thing beside
entity Thing passed with zero errors. It is now an Error.
This is the rule behind a class of confusing failures: with both defined,
a reference such as Id(Thing) had two plausible referents, and which one
it found was not something the model stated.
A definition keyword may not be a bare identifier¶
Eleven keywords introduce a definition, and none may be used as a bare name:
domain, context, entity, adaptor, saga, epic, projector,
repository, streamlet, handler, function
Two escapes, both legal:
handler 'projector' is { ??? } // quoted identifier
handler Projector is { ??? } // the check is case-sensitive
Nothing is wrong with the names — the point is that two introducing keywords in a row leave a reader, and any tool, guessing which word is the keyword. The quoted form says which.
This is deliberately not all 156 keywords. version and copyright, in
particular, remain usable as field and type names; that was a compatibility
decision when they were added, and it stands. The ambiguity only bites where a
word would otherwise start a definition, so that is where the rule bites.
Containment Rules¶
RIDDL follows strict containment rules that define what elements can be defined within other elements.
Root¶
A Root can contain Domains, Modules, Authors, Versions, Copyrights,
Comments, include directives, and import directives.
Module¶
A Module is flat: it can contain any top-level definition, in any order —
types and the four message kinds, constants, invariants, users, contexts,
entities, adaptors, functions, projectors, repositories, streamlets, sagas,
epics, connectors, relationships, authors, versions, copyrights, nested
modules, include and import.
Domain¶
A Domain can contain:
- Types
- Authors
- Contexts (bounded contexts)
- Domains (nested subdomains)
- Users (actors who interact with the system)
- Epics (user stories with use cases)
- Sagas (multi-step processes)
- Repositories (when they synthesize across several contexts)
- Connectors (when they join ports across contexts)
- Versions and Copyrights
- Imports (from
.bastfiles) - Includes (file inclusion)
Context¶
A Context (bounded context) can contain:
- Everything in processor definition contents (below)
- Entities
- Repositories
- Projectors
- Sagas
- Adaptors (message translation between contexts)
- Streamlets and generic processors
- Connectors (linking outlets and inlets)
- Groups (UI components, also called page, pane, dialog, etc.)
- Imports and Includes
Entity¶
An Entity can contain:
- Everything in processor definition contents
- States (with their own handlers and invariants)
- Includes
Processor Definition Contents¶
Every processor — Context, Entity, Adaptor, Projector, Repository, Streamlet — can contain:
- Types
- Constants
- Invariants
- Functions
- Handlers
- Inlets and Outlets
- Nested processors, streamlets, connectors and relationships
- Versions and Copyrights
- Comments and Includes
Leaf Definitions¶
Authors, Users, Terms, Constants, Versions and Copyrights contain only their metadata and cannot contain other RIDDL definitions.
Type System¶
Basic Types¶
UUID,String(min, max),Integer,Decimal(whole, fractional)Boolean,Pattern("regex"),Real,Natural, etc.
Numeric arguments accept a sign: range(-5, 5) and Decimal(-3, 2) mean what
they say.
Complex Types¶
- Record Types: Named collections of fields
record Address is {
street1 is String
city is String
state is String
zipCode is String
country is String
} with {
briefly as "Physical mailing address"
described by {
| Represents a physical address with standard components
| used for shipping and billing purposes.
}
}
- Enumerations:
- Collections:
The yields and replies Clauses¶
A command or query may declare the response it produces. This makes the request/response pairing declarative, so generators can emit precise signatures. RIDDL has two such pairings, and each has its own keyword:
command PlaceOrder yields event OrderPlaced is {
cartId is CartId
}
query GetOrder replies result OrderInfo is {
orderId is OrderId
}
A command yields an event; a query replies a result. The two are
genuinely different acts — emitting an event as a consequence of a command is
not the same thing as answering a question — and a generator lowers them
differently. Crossing them is an Error: yields on a query, replies on a
command, or either naming a type of the wrong kind.
Changed in RIDDL 2.0
Before 2.0 both were spelled yields, and the reply statement was a
deprecated synonym for yield that parsed to the same node. They are now
distinct, and the switch is hard: yields result is an Error
immediately, with no transitional period.
The declaration side was effectively empty in practice — commands widely
declared yields event, while almost no query declared its result — so
replies fills a gap rather than breaking established models.
Both are optional. When declared, the handler for that message must respond
with exactly that message; when not declared, a handler may respond as it
likes. The one place replies is required is a query that something
asks — an ask takes the answer's type from the query's replies, so
there is nothing to read without it.
Type Validation
Errors:
- Defining a type whose name exactly matches a predefined type name
(e.g.,
type Currency is Decimal(10,2)) — this shadows the built-in type and is not allowed yieldsorreplieson a type that is not a command or query- A command whose
yieldsnames a non-event, or a query whoserepliesnames a non-result - Pairing the wrong keyword with the use case:
yieldson a query, orreplieson a command
Style Warnings:
- Defining a type whose name is a case-variant of a predefined type
(e.g.,
type timestamp is TimeStamp)
Completeness Warnings:
- Command types with no fields (excluding
???placeholders) - Event types not produced by any handler
- Query types without corresponding result types (and vice versa)
Contexts and Intentions¶
A context may declare its intention — what kind of bounded context it is — with a keyword prefix:
type Request is String
application context Storefront is { ??? }
external context StripePayments is { ??? }
// gateway and service carry SHAPE requirements -- see below
gateway context PublicApi is {
inlet fromWeb is type Request
inlet fromMobile is type Request
outlet inbound is type Request
}
service context Pricing is {
inlet request is type Request
outlet response is type Request
}
| Intention | Meaning |
|---|---|
application |
Presents a user interface. The only place UI may be modeled. |
external |
A third-party system the model does not own. |
gateway |
An entry point that adapts the outside world to the inside. |
service |
An internal service with no user interface. |
The intention is optional; a plain context declares no intention and is
subject to no intention rules.
Intention Validation
Errors:
- A context that contains a
group(or any of its UI aliases) but is not anapplicationcontext. UI belongs at the application boundary. - A
gatewaycontext that is not a merge — it must have 2 or more inlets and exactly 1 outlet. A gateway funnels several outside channels into one inside path, so a portlessgateway context G is { ??? }is an Error: "Gateway context 'G' must have a merge shape (>=2 inlets, 1 outlet) but is void". - A
servicecontext that is not a flow — exactly 1 inlet and 1 outlet. A service takes a request and returns a response. - An
externalcontext modeling persistence it cannot own
application and external carry no shape requirement, so a
placeholder application context A is { ??? } is fine. gateway and
service cannot be stubbed that way: declaring the intention commits you
to the ports.
The gateway, service, external and wrapper options are deprecated
These were previously spelled as options in the with { } block. Use the
intention prefix instead. The options still parse and emit a
[deprecated] message.
Entities and States¶
Entities are stateful objects with explicit states. Each state references a record that defines its data structure, and can optionally contain handlers and invariants that apply while the entity is in that state:
entity Product is {
initial state ProductData of record ProductRecord is {
invariant PriceIsPositive is price > MinimumPrice
handler ProductHandler is {
on cmd: command UpdatePrice {
set field ProductRecord.price to cmd.newPrice
yield event PriceUpdated(productId = cmd.productId,
newPrice = cmd.newPrice)
}
}
} with {
briefly as "Product state with update handling"
}
} with {
briefly as "Represents a purchasable item"
}
States can also be defined without a body when handlers are defined at the entity level instead:
entity SimpleProduct is {
state ProductData of record ProductRecord
handler ProductHandler is { ??? }
}
The initial Marker¶
An optional initial keyword before state or handler marks the entity's
starting state, and the handler that is live after a morph:
entity Order is {
initial state Pending of record PendingData is {
initial handler PendingHandler is { ??? }
}
state Active of record ActiveData is { ??? }
}
Unmarked models keep the previous "first one declared wins" behavior, so this is fully backward compatible. Marking it explicitly makes the model safe to reorder.
Declaring more than one initial state in an entity, or more than one
initial handler in a state, is an Error.
Finite State Machines¶
When an entity has multiple states with their own handlers, it models a finite
state machine — each state responds to messages differently, and the morph
statement transitions between states:
entity Order is {
initial state PendingOrder of record PendingOrderData is {
handler PendingHandler is {
on command ConfirmOrder {
morph entity Order to state Order.ActiveOrder
with record ActiveOrderData()
}
}
}
state ActiveOrder of record ActiveOrderData is {
handler ActiveHandler is {
on command ShipOrder {
morph entity Order to state Order.ShippedOrder
with record ShippedOrderData()
}
}
}
state ShippedOrder of record ShippedOrderData is {
handler ShippedHandler is { ??? }
}
} with {
option is finite-state-machine
}
Entity Validation
Errors:
- More than one
initialstate, or more than oneinitialhandler in a state - Defining a type whose name exactly matches a predefined type
Warnings:
- Entity Id type defined inside the entity body instead of the containing context (move it to the context level)
- Entity Id type defined at domain level or beyond (scope too broad)
- Entity Id type not defined at all
Completeness Warnings:
- States without an
on initclause on initwithout asetstatement- Command handlers that neither
sendnoryieldan event, and do not refuse. A clause that refuses witherrororrequirehas processed the command — it decided, it declined, and there is nothing to record — so it discharges the obligation. Previously this flagged the honest refusal-only clause, and was silenced by adding asendafter the refusal, which the refusals-before-effects rule makes unreachable: it rewarded exactly the dead code a modeller should avoid. - Query handlers that don't
yieldorsenda result - Entities without any
on queryclause - Entities with no handlers at all
- FSM entities (2+ states) without
morphorbecomestatements - Empty handlers (no statements or only
???) - Handlers containing only
dostatements - Empty
on otherclauses (silently discards messages)
Commands and Events¶
Commands represent requests to change state:
command UpdatePrice yields event PriceUpdated is {
productId is ProductId
newPrice is Price
} with {
briefly as "Command to change a product's price"
}
Events represent state changes that have occurred:
event PriceUpdated is {
productId is ProductId
oldPrice is Price
newPrice is Price
} with {
briefly as "Event indicating a product price change"
}
Command-Event Relationship¶
Commands should always result in one or more events being emitted. This follows the reactive principles of RIDDL:
handler ProductCommandHandler is {
on cmd: command UpdatePrice {
when cmd.newPrice > MinimumPrice then
yield event PriceUpdated(productId = cmd.productId,
newPrice = cmd.newPrice)
else
error "Price must exceed the minimum"
end
}
}
Handlers¶
Handlers define how processors respond to messages. They contain on clauses
that match specific message kinds and execute statements.
On Clause Types¶
| Clause | Purpose | Example |
|---|---|---|
on command X |
Handle a specific command | on command CreateOrder { ... } |
on event X |
Handle a specific event | on event OrderCreated { ... } |
on query X |
Handle a query and yield a result | on query GetOrder { ... } |
on result X |
Handle a result | on result OrderInfo { ... } |
on init |
Execute when the processor initializes | on init { ... } |
on term |
Execute when the processor terminates | on term { ... } |
on activate |
Execute when an entity is rehydrated | on activate { ... } |
on passivate |
Execute when an entity is evicted | on passivate { ... } |
on other |
Handle any unmatched message | on other { ... } |
on init and on term are once-ever lifecycle events. on activate and
on passivate fire on every rehydration and eviction, are entity-only, and
must be side-effect free: send, tell, yield, morph and become are
rejected inside them at parse time.
Naming the Handled Message¶
An on clause may bind a local name to the message it is handling, using
ordinary type ascription. Within the body the name denotes the whole message:
handler H is {
on ord: command PlaceOrder {
yield event OrderPlaced(id = ord.id, total = ord.total)
}
}
The binding is optional, so every existing model parses unchanged. A local name that shadows an outer definition is legal but draws a Warning; a name that does not begin with a lowercase letter draws a StyleWarning.
Message Origins¶
An on clause may name where the message came from:
Basic Handler Example¶
handler ProductCommandHandler is {
on upd: command UpdatePrice {
when upd.newPrice > Zero then
set field price to upd.newPrice
send event PriceUpdated(productId = upd.productId) to outlet Events
else
error "Price must be greater than zero"
end
}
on query GetProduct {
reply result ProductInfo()
}
} with {
briefly as "Processes commands for product management"
}
Handler Locations¶
Handlers can be defined at multiple levels:
- Entity handlers: Default message handling for the entity
- State handlers: Message handling specific to a state
- Context handlers: API-level handlers for the bounded context
- Processor handlers: Handlers in Repository, Projector, Adaptor, etc.
An entity-scope handler may be marked initial, which makes it the initial
handler for every state that does not define one of its own — a default
across the whole state machine rather than a per-state choice. Where no marker
appears, an entity-scope handler is defaulted to initial only when the entity
has exactly one state.
Declaring more than one initial handler at entity scope is an Error,
regardless of how many states the entity has. Verified against
2.0.0-rc.9-54.
Handler Kind Rules
Errors, enforced at parse time where possible:
- A projector is event-only:
on command,on queryandon recordare rejected.on eventandon resultare valid. requireanderrorare forbidden inon event— an event has already happened and must always be accepted.on activate/on passivateoutside an entity, or containing an effect statement.- An adaptor handler with no
on otherclause. An adaptor must say explicitly what it does with messages it does not recognize.
Style Warnings:
- Two
onclauses in one handler that handle the same message. The later clause is unreachable.
Invariants¶
An invariant is a named boolean rule. Its full form is:
Implicit Application¶
An invariant applies to every clause of its declaring scope, checked as a precondition before any effect in that clause. No statement at the point of use is needed.
Changed in RIDDL 2.0
Previously an invariant did nothing unless a clause named it in require
invariant X, which made it easy to carry a constraint that read as enforced
and was inert. Every invariant now applies — including ones you also
require explicitly, because otherwise adding a single require would
silently narrow a rule from enforced-everywhere to enforced-there.
Condition Forms¶
invariant Legacy is "the account must be in good standing"
invariant InStock is quantity >= Zero
invariant CanCoverFee is {
let available = call function Available(held = holdAmount, total = balance)
available >= minimumFee
}
No numeric literals in a condition
The boolean sub-language has no numeric literal atom, so amount >= 0 does
not parse — compare against another named value instead (amount >= floor).
Arithmetic is not available either: a let binds a reference or a call,
not an expression such as balance - holdAmount.
A literal string is an AI-fill site. A boolean expression or a block already is the predicate. The block form admits the statements a pure function may contain, ending in the boolean that is the predicate.
Scope and Readable Fields¶
The declaration decides both — never the call site:
| Declaration | Applies to | May read |
|---|---|---|
in an entity, no requires |
every clause of that entity, including its states' handlers | fields present in every state record |
| inside a state S | that state's handlers only | S's record fields |
in an entity, requires state S |
that entity's clauses while in state S | S's record fields |
requires <type T> |
nothing implicitly — explicit only | the value handed to it |
| on a context or other stateless processor | nothing implicitly — explicit only | the value handed to it |
The intersection rule. An entity-level invariant reading balance forces
every state record of that entity to have a balance field. Referencing a field
absent from any state record is an Error.
A stateless processor has no ambient data for an implicit predicate to read, so an invariant declared on one must name what it needs and be invoked explicitly:
record Limits is { ceiling: Integer, used: Integer }
invariant UnderLimit requires record Limits is used <= ceiling
The clause then hands it the value:
Purity¶
An invariant block may contain only the statements a pure function may contain:
no state writes, no send/tell, no morph/become/yield/reply. An
invariant may not send even a read-only query — doing so would make the
predicate asynchronous, fallible, non-deterministic and no longer structurally
terminating. The clause gathers; the invariant receives.
A function may read no entity state at all; an invariant may read the state its declaration names. That asymmetry is deliberate — an invariant is a predicate over state, evaluated inside the entity's single-writer window, with its readable fields bounded by its declaration.
Failure Modes¶
| Applied | Failure is a | Result |
|---|---|---|
explicitly, at require invariant X |
refusal | modeled outcome — an error result naming the rule |
| implicitly | fault | exception plus rollback; does not widen the clause's result type |
Invariants in scope are checked in declaration order; the first failure is reported.
Implicit invariants are skipped in on init — that clause is where state
comes into existence. They apply in on term, and they apply in on
event clauses. The last is not a contradiction of the rule forbidding
authored require/error in on event: that rule bars refusing an event,
and an implicit violation faults and rolls back rather than refusing, so the
event remains a fact.
Diagnostics¶
- An invariant declaring
requires <type T>that norequire invariant X with <expr>ever invokes draws a warning — it is inert. - An entity-level invariant referencing a field absent from any state record is an Error.
See the Invariant concept page for the reasoning behind these rules.
Value Expressions¶
RIDDL 2.0 introduces a real value-expression system. Wherever a statement needs a value, it accepts any of these forms:
| Form | Syntax | Meaning |
|---|---|---|
| Literal | "some text" |
Opaque pseudo-code or a literal constant |
| Value reference | order.total |
A named field, state field, function input, or let local |
| Constructor | OrderPlaced(total, id = x) |
Builds a message or record |
| Get | get from input SignupForm |
Reads a UI input or an entity state |
| Call | call function Pricing.Total(a, b) |
Invokes a pure function for its result |
| Prompt | prompt("compute the discount") |
A value computed by AI at generation time |
| Boolean | a > b and not c |
A structured boolean expression |
Constructors¶
A constructor builds a message or a record inline. Arguments are positional first, then named:
An empty argument list is an arity of zero and must match like any other,
so OrderPlaced() against a message that has fields is an Error.
Argument count, names, ordering and (best effort) types are all checked against the target's fields.
Get¶
get from reads a value from a UI input or an entity state:
Call¶
call invokes a function — and only a function, because functions are the
only pure definitions — and produces its result:
Calling something with no declared returns is an Error.
Boolean Expressions¶
Boolean expressions have the usual precedence: or < and < not <
comparison < atom. Parentheses group.
An invariant takes one too, though it is a definition rather than a statement:
and, or, not, true and false are context-sensitive: they are
recognized only inside a boolean expression, so they remain legal identifiers
everywhere else.
Comparisons are type-safe
Both operands of a comparison must be typed references — a value
reference, a get from, or a named constant. A literal is not permitted:
count > "5", count > 5, count > true and count > R(1) all fail at
parse time.
To compare against a fixed value, name it:
constant MaxItems is Natural = "100"
// ...
when cart.itemCount > MaxItems then error "too many items" end
This is deliberate. It removes magic constants from models and makes every
comparison check the types on both sides. == and != require operands of
the same category; <, >, <= and >= require an ordered (numeric)
type on both sides.
Statement Syntax¶
Morph Statement¶
Changes entity state. The payload is a record — a bare record reference or an inline constructor:
Send Statement¶
Emits a message on one of this processor's own outlets. A connector then routes it to a downstream inlet:
send event ItemAdded to outlet CartEvents
send command ProcessPayment(orderId) to outlet PaymentRequests
send … to inlet is deprecated
Sending directly into another processor's inlet bypasses the streaming
model — that is tell's job. outlet is the canonical send target. The
inlet form still parses and emits a [deprecated] message; it is slated
for removal in 3.0.
Tell Statement¶
Delivers a message directly to a processor:
Validation
A tell whose target is not reachable through any modeled connector draws
a Warning.
Yield and Reply Statements¶
These are the statement halves of the two pairings described under
yields and replies. Both produce a
declared response without the handler needing to know the sender's identity:
yield event OrderPlaced(id, total) // in a command handler
reply result ProductInfo(id, name) // in a query handler
yield emits a command's declared event. reply answers a query with
its declared result. Each satisfies the completeness check for its own kind of
handler, and the wrong pairing — yield result or reply event — is an Error.
Changed in RIDDL 2.0
Until 2.0 reply was a deprecated synonym for yield, parsing to the
same node. That is now reversed: reply is a first-class statement with
its own meaning, and yield result no longer parses as valid. If you are
migrating a 1.x model, reply is a destination rather than something to
rewrite away.
Set Statement¶
Assigns a value to a field or a state:
set field status to "Active"
set field total to call function Pricing.Total(subtotal, tax)
set state ActiveOrder to record ActiveOrderData()
Let Statement¶
Creates a local variable binding, with an optional type annotation. When no annotation is given, the type is inferred from the bound expression:
let totalPrice = call function Cart.Total(subtotal, tax, shipping)
let discount: Decimal = "totalPrice * 0.1"
let ready = order.isPaid and not order.isCancelled
A let is lexically scoped and statement-ordered: it is visible only after its
declaration and is shadowed inside nested blocks.
Ask¶
ask states that two messages are two halves of one interaction: a query
sent to a processor, and the reply that answers it. It is a value, not a
statement, so it is used through let:
Before ask, a model could say a message was sent (tell) and what handling
one produces (yields/replies), but not that a caller awaits a particular
answer. yield names no destination and tell says nothing about a reply, so
a generator could not tell fire-and-forget from request/response.
ask declares that correlation and nothing else. It deliberately implies
no mechanism — not a future, a temporary actor, a correlation-id field, or a
blocking call. All four are lowerings a generator may choose, on the same
principle as message_envelope: RIDDL specifies meaning, generators choose
representation. Timeouts are likewise a generated-code concern, not language.
Queries only, structurally. The operand is a query reference, not a general message reference — a command, event, result or record is not answerable, so the restriction is in the shape of the syntax rather than in a validation rule.
The answer's type is always known: it is the query's declared
replies result X. This is the one place
replies is mandatory, which is why the requirement sits at the ask site
rather than on every query.
Errors
- Asking a query that declares no
replies— there is no type to give the answer. - Asking a processor that has no clause handling the query. An
on otherclause counts as handling everything, and an entity's handlers may live under a state. - Asking anywhere inside a saga step, including as a value nested in a larger expression. See below.
An ask whose callee refuses is expected, not an error: recording a rejection settles the obligation, consistent with the discharge rule.
A saga may not ask
A saga must not depend on dynamic state, or the same inputs could yield different transaction results at different times. Compensation makes this sharper: if the forward action read a value that has since changed, the undo would be reversing something other than what happened.
The remedy is to acquire the value in a handler and pass it into the saga
through the saga's requires, so the saga is closed over its inputs and
compensation sees the same data the forward action saw.
The prohibition reaches every ask embedded in a value expression, at
any depth — it is not enough to keep ask off the right-hand side of a
let.
Put Statement¶
Publishes a value to a UI output. Valid only in application and context handlers:
Return Statement¶
Returns a function's result. Valid only in a function body:
When Statement¶
Conditional logic. The end keyword is required:
A condition may be any of:
- A boolean expression:
when a > b and not c then - A bare boolean-typed reference, including a dotted path:
when order.isPaid then - A
letbinding, optionally negated:when authorized then,when !authorized then - An AI-evaluated prompt:
when prompt("the user is authenticated") then
A bare reference is resolved and checked to be Boolean-typed; a clearly non-Boolean condition is an Error.
A bare string condition is deprecated
when prompt("the user is authenticated") then still parses but draws a
[deprecated] message. Write when prompt("…") instead.
The reason is consistency: everywhere else in RIDDL a bare quoted string
denotes a literal value, while prompt(…) marks something an AI
decides. A natural-language condition is plainly the latter, so it should
say so rather than borrow the literal's spelling.
An else clause handles the false case:
let authorized = user.hasPermission
when authorized then {
send event ActionCompleted to outlet Events
} else {
error "User not authorized"
} end
Match Statement¶
Pattern matching over a typed subject. The subject is a value reference, a
get from, or a legacy pseudo-code literal:
match orderStatus {
case Pending {
tell event OrderPending to entity Order
}
case Shipped when order.isPaid {
tell event OrderShipped to entity Order
}
default {
error "Unknown order state"
}
}
A comparison case needs a numeric subject, so it belongs to a different match than the one above — the subject's type decides which case forms are legal on it:
match order.total {
case >= HighValueThreshold {
tell command EscalateReview to entity Review
}
default { do "no escalation needed" }
}
A case pattern is one of:
- Type case — a bare type reference matching an alternant, enumerator or
message subtype:
case Pending - Comparison — an operator and a comparand, with the subject as the
implicit left operand:
case >= HighValueThreshold - Literal — a legacy pseudo-code label:
case "pending"
Each case may carry an optional when <boolean> guard.
Naming an unknown type-case is an Error. For a closed subject — an
Enumeration or Alternation — a non-exhaustive match without a default draws a
StyleWarning.
Foreach Statement¶
RIDDL's safe, bounded loop. There is no unbounded iteration in the language:
foreach line in field order.lines {
send event LineShipped(sku = line.sku) to outlet Shipments
}
foreach item in myLocalCollection {
do "record the item"
}
The collection is a field reference or a let-bound local whose type resolves
to a collection — Sequence, Set, Graph, Table, Replica, Mapping, or a
cardinality wrapper such as many or optional. Any resolvable path that
lands on a collection is accepted; it need not be a direct field of the
enclosing state, which is why order.lines above works.
The element name is bound over the loop body and carries the element's
type, so line.sku resolves and line.nosuch is an Error. The binding ends at
the closing brace.
Destructuring a mapping¶
A mapping yields a key and a value, so iterating one binds two names:
Arity is a validation rule rather than a grammatical one — both shapes parse — and it is checked in both directions:
| Mistake | Message |
|---|---|
| one name over a mapping | binds a key AND a value, so it needs two names |
| two names over a non-mapping | binds a second name only over a mapping |
Require Statement¶
Asserts a precondition that must hold before proceeding:
require amount > Zero
require "the customer is in good standing"
require invariant BalanceNonNegative
require invariant UnderLimit with limits
The with <value> form hands a value to an invariant that declares
requires <type> — the one invariant form ambient scope cannot supply. See
Invariants.
In a when or match condition the same invariant needs no argument, and
the invariant keyword itself is optional there. The asymmetry is deliberate:
a condition asks whether the rule holds, while a require applies it and
so must be handed what the rule reads.
Naming an invariant does not switch it on
As of RIDDL 2.0 an invariant applies implicitly across its declaring
scope, whether or not any clause names it. A require invariant X is an
explicit restatement at a point you want the check called out — never what
activates it. The rule that an unreferenced invariant draws a UsageWarning
is withdrawn; the only inert form left is an invariant declaring
requires <type> that nothing ever invokes, and that still warns.
Error Statement¶
Refuses to proceed, with a reason:
Refusals Before Effects¶
Within any single linear statement list, every refusal (require, error)
must come before every effect (set, morph, become, send, tell,
yield, put). Performing effects and then refusing would leave partial
changes behind.
Each statement list is checked independently, so each branch of a when,
match or foreach body is its own list. A refusal after an effect in the same
list is an Error.
on cmd: command Withdraw {
require cmd.amount > Zero // refusals first
require balance >= cmd.amount
set field balance to "balance - amount" // then effects
yield event Withdrawn(amount = cmd.amount)
}
Do Statement¶
Describes an action in natural language for later implementation:
This is useful for describing business logic that will be implemented in target code.
The prompt statement is deprecated
do is canonical. prompt "..." still builds the same node and emits a
[deprecated] message; prettified output normalizes it to do.
Do not confuse it with the prompt(...) value, which is a different
construct distinguished by its parentheses — it denotes a value computed by
AI rather than an action described for a human.
Code Statement¶
The code statement is RIDDL's deliberate escape hatch: an opaque
pass-through of raw target-language source, handed to the code generator
untouched.
It is written as a fenced block with a language tag:
The language tag must be one of exactly four: scala, java,
python, mojo.
RIDDL does not parse, check, or understand the contents. Everything between the fences is carried through verbatim.
Where it is allowed¶
Everywhere. Unlike every other statement, code is subject to none of the
scope rules:
| Rule | Applies to code? |
|---|---|
| Functions must be pure | No — code is legal in a function body |
on activate / on passivate must be side-effect free |
No — code is legal there |
| Refusals must precede effects | No — code is neither, so it never trips the check |
That is not an oversight. RIDDL cannot classify opaque source: it has no way to know whether a block of Scala writes state, sends a message, or computes a number, so it declines to guess rather than guessing wrong.
Using it opts out of the guarantees
Every rule above exists to give a model a property worth having — a
function that is provably pure, a lifecycle clause that provably does not
emit, a handler that provably refuses before it acts. A code block
suspends those guarantees for as long as it lasts, and it makes the model
specific to one target language.
That is a real trade and worth making deliberately. Prefer
do "..." to describe intent and let the generator
implement it; reach for code when a generator genuinely cannot express
what you need.
The tag is matched by prefix
The parser matches the four tags as prefixes, so javafoo and pythonic
are currently accepted, with the surplus text treated as part of the code
body. Do not rely on this — only the four tags above are supported, and
the leniency may be tightened.
Become Statement¶
Switches the live handler of an entity:
Functions¶
Functions define reusable, pure operations. A function's requires and
returns may name an existing type rather than spelling out an aggregation,
which makes unary and nullary functions natural:
function CalculateTotal is {
requires record TotalInputs
returns Price
return call function Tax.Apply(subtotal)
} with {
briefly as "Calculates the final cart total"
}
The inline aggregation form still works:
function CalculateTotal is {
requires { subtotal is Price, taxes is Price, shipping is Price }
returns { total is Price }
do "Add subtotal, taxes and shipping"
}
Functions must be pure
A function body may not write entity state (set, morph, become), nor
send, tell or yield. This is enforced at parse time, so an effect
statement can never enter a function's AST.
Refusals (require, error) and pure computation (let, when, match,
foreach, do, return, code blocks) remain legal.
The inline requires { … } form is deprecated
Naming a type is preferred. The inline aggregation emits a [deprecated]
message but continues to work.
Sagas¶
Sagas coordinate multi-step processes with compensation:
saga CheckoutProcess is {
requires record CheckoutInputs
returns record CheckoutOutcome
step TakePayment is {
tell command ProcessPayment(orderId) to entity PaymentService
} reverted by {
tell command RefundPayment(orderId) to entity PaymentService
} with {
briefly as "Processes payment for the order"
}
step ReserveStock is {
tell command ReserveItems(orderId) to entity Inventory
} reverted by {
tell command ReleaseItems(orderId) to entity Inventory
} with {
briefly as "Holds the items until payment settles"
}
} with {
option is compensate
briefly as "Orchestrates the checkout process steps"
}
Saga Options¶
| Option | Meaning |
|---|---|
compensate |
On failure, run the accumulated steps' undo blocks in reverse |
parallel |
Start all steps at once; the coordinator gathers results asynchronously. Any one failure compensates in reverse order of the original sends. |
A saga is sequential by default, so there is no sequential option.
Saga Validation
Errors:
- A saga step that references a definition owned by a different domain. A saga orchestrates a transaction within one bounded domain.
Warnings:
- A step's do-block containing more than one potential failure point.
A step's do/undo is all-or-nothing, so it should have at most one place
it can fail — split the step.
send,tell,yieldandputcan fail, as can each embeddedcallorget.
Completeness Warnings:
- Saga step do-statements that drive no command
Repositories¶
Repositories define persistence:
repository CartRepository is {
schema CartData is relational of
cart as record Cart
link cartItems as field Cart.items.id to field Product.id
handler CartRepositoryHandler is {
on event CartCreated {
do "Persist the new cart record to the database"
}
on other { error "Unrecognized message" }
}
} with {
briefly as "Persistent storage for shopping cart data"
}
Repositories at Domain Scope¶
A repository that synthesizes messages from entities across several contexts, and is queried by several contexts, may be declared directly in a Domain rather than in a Context.
Repository Scope Validation
A repository's reach is the set of contexts owning the messages its
on clauses handle.
- Error: a domain-scoped repository reaching only ONE context. It is provably unnecessary at domain scope — move it into that context.
- CompletenessWarning: a context-scoped repository reaching another context. Consider promoting it to domain scope.
Adaptors¶
Adaptors translate messages between bounded contexts. They handle the transformation needed when one context communicates with another that uses different message formats or terminology.
Direction¶
Adaptors specify a direction relative to a context:
from context X: Receives messages from context X, translates for local useto context X: Sends messages to context X, translating from local format
Example¶
context OrderContext is {
event OrderPaymentReceived is { orderId is UUID }
outlet OrderEvents is type OrderPaymentReceived
adaptor PaymentIntegration from context PaymentContext is {
handler InboundPayments is {
on evt: event PaymentContext.PaymentCompleted {
let orderId = evt.reference
send event OrderPaymentReceived(orderId) to outlet OrderEvents
}
on other {
error "Unrecognized payment event"
}
}
} with {
briefly as "Translates payment events for order processing"
}
}
The Isolation Seam
An adaptor bridges exactly two contexts: its parent context and its referent context. It is the only sanctioned crossing point between contexts, so it must not traffic in a third context's messages.
A message whose owning context is neither the parent nor the referent is an Error. Types defined at domain or root level are shared vocabulary common to both sides and are never flagged.
Every adaptor handler must also have an on other clause.
Projectors¶
Projectors transform and aggregate events into read models. They are key to implementing CQRS, where the write model (entities) is separate from the read model (projections).
Purpose¶
- Project (transform) events from multiple sources into denormalized views
- Aggregate data for efficient querying
- Maintain materialized views updated by event streams
Example¶
context ReportingContext is {
projector SalesDashboard is {
record DailySales is { day is Date, total is Decimal(10, 2) }
updates repository SalesData
handler SalesEventHandler is {
on event OrderContext.OrderCompleted {
do "Update daily sales totals with order amount"
}
on event OrderContext.OrderRefunded {
do "Subtract refund amount from daily totals"
}
}
} with {
briefly as "Projects order events to sales dashboard"
}
}
Projectors are event-only
on command, on query and on record are rejected at parse time
inside a projector. Only on event and on result are valid.
Completeness Warnings:
- Projectors not referencing any repository
- Projector handlers not
telling to a repository - Declared repository references never used in
tell
Streaming Processors¶
Streaming processors define components for building data pipelines with typed input ports (inlets) and output ports (outlets).
Inlets and Outlets¶
- Inlet: A typed input port that receives messages
- Outlet: A typed output port that sends messages
Every processor kind may declare ports — not just streamlets. An entity may own an outlet; a projector may own an inlet.
context DataPipeline is {
processor OrderEventSource as source is {
outlet OrderEvents is type OrderEvent
} with {
briefly as "Streams order events from the event store"
}
processor OrderEnricher as flow is {
inlet RawOrders is type OrderEvent
outlet EnrichedOrders is type EnrichedOrderEvent
handler EnrichmentHandler is {
on event OrderEvent {
do "Look up customer details and product information"
send event EnrichedOrderEvent to outlet EnrichedOrders
}
}
}
processor AnalyticsSink as sink is {
inlet AnalyticsEvents is type EnrichedOrderEvent
handler AnalyticsHandler is {
on event EnrichedOrderEvent {
do "Send event to analytics platform"
}
}
}
}
Portlet Options¶
| Option | Applies to | Meaning |
|---|---|---|
async |
Inlet, Outlet | A deliberate codegen async boundary; the generator inserts a real boundary here rather than fusing the stream |
ordered |
Connector, Inlet | Delivery preserves order |
unordered |
Connector, Inlet | Delivery order is not significant |
Streaming Validation
Errors:
- An ascribed shape that contradicts the declared port arity
- A port referenced by more than one connector (see below)
Style Warnings:
- A processor with at least one port and no
as <shape>ascription - A connected pipeline in which every portlet is
async. Nothing can be fused, so the stream pays message-passing overhead at every boundary and typically runs slower than a fused one.
Completeness Warnings:
- Isolated processors not connected to any connector
- Sources without a downstream path to a sink
- Sinks without an upstream path from a source
- Unattached inlets and outlets
- Flow/Split/Router handlers that don't send to outlets
Connectors¶
Connectors link an outlet to an inlet, defining how data flows between processors.
Syntax¶
Example¶
context DataPipeline is {
connector OrderFlow is
from outlet OrderEventSource.OrderEvents
to inlet OrderEnricher.RawOrders
with {
briefly as "Connects order source to enrichment"
}
}
Port Cardinality¶
Exactly one connector may attach to any given port. Fan-in and fan-out are
modeled by declaring multiple ports — a merge or split shape derives from
its arity — never by attaching several connectors to a single port.
More than one connector on a port is an Error.
Connector Scope¶
A connector may be declared in a Context or, when it joins ports in two different contexts, in the enclosing Domain.
Connector Placement Validation
- Error: the two ends resolve to different domains. A stream edge across a domain boundary is a failure of domain analysis.
- Error: a domain-scoped connector whose ends share one context (over-scoped) — move it into that context.
- Error: a context-scoped connector whose ends cross contexts (under-scoped) — promote it to domain scope.
- CompletenessWarning: a domain-scoped cross-context connector without
the
persistentoption. Durability at a context boundary can be model correctness, not merely deployment.
Pipeline Pattern¶
context EventProcessing is {
processor Events as source is { outlet Raw is type RawEvent }
processor Validate as flow is {
inlet In is type RawEvent
outlet Out is type ValidatedEvent
}
processor Store as sink is { inlet In is type ValidatedEvent }
connector Step1 is from outlet Events.Raw to inlet Validate.In
connector Step2 is from outlet Validate.Out to inlet Store.In
}
The Standard Module¶
Every model has access to a predefined module named Riddl, with no import and
no author declaration required. It provides the two stream terminators that the
one-connector-per-port rule makes structurally necessary:
| Definition | Kind | Purpose |
|---|---|---|
BottomlessPit |
sink | Consumes everything delivered to its inlet hole and emits nothing |
ForeverEmpty |
source | Never produces anything on its outlet void |
Drain |
type | Anything, so one drain absorbs every message type |
Reaching BottomlessPit terminates a pipeline exactly as a modeled sink does,
and ForeverEmpty originates one, so neither draws reachability warnings. The
port-cardinality rule is waived for them: many connectors may share the
universal drain.
A model that ignores the standard module is completely unaffected by it — the module is never added to your model's contents.
UI Components¶
RIDDL supports UI modeling. UI may only be modeled inside an application
context.
application context Storefront is {
command PlaceOrder is { cartId is UUID }
record PaymentDetails is { cardToken is String, amount is Decimal(10, 2) }
page ProductDetails is { ??? } with {
briefly as "Page showing product information"
}
page ShoppingCart is {
button Checkout activates command PlaceOrder with {
briefly as "Checkout button to proceed to payment"
}
}
page Payment is {
form PaymentEntry submits record PaymentDetails
}
} with {
briefly as "User interface components for the system"
}
An input element always names a defined type — a command it triggers, or a
record it submits. A predefined type such as Boolean will not do: the
reference is resolved against the model, and predefined types are not
definitions in it.
Element Aliases¶
| Role | Keyword and aliases |
|---|---|
| Group | group, page, pane, dialog, menu, popup, frame, column, window, section, tab, flow, block |
| Output | output, document, list, table, graph, animation, picture |
| Input | input, form, text, button, picklist, selector, item |
Interaction Verbs¶
- Presentation (outputs):
presents,shows,displays,writes,emits - Acquisition (inputs):
acquires,reads,takes,accepts,admits,enters,provides,selects,chooses,picks,initiates,submits,triggers,activates,starts
UI Validation
Errors:
- A
group(or alias) in a context that is not anapplicationcontext
Style Warnings:
- An input using a selection verb (
selects,chooses,picks) whose type is not a choice among options — that is, not an Enumeration or Alternation
Epics and Use Cases¶
Epics model user stories:
epic ShoppingCartEpic is {
user Customer wants to "add items to a shopping cart"
so that "they can purchase multiple items at once"
case AddingToCart is {
user Customer must "add products to cart"
so that "they can purchase them later"
step focus user Customer on page Storefront.ProductDetails
step take input Storefront.ProductDetails.AddToCartForm from user Customer
step send command AddToCart from context Storefront to entity Cart
step show output Storefront.ShoppingCart.CartSummary to user Customer
step entity Cart refuses user Customer "the item is out of stock"
} with {
briefly as "Adding products to the shopping cart"
}
}
Two rules shape those steps. Inputs and outputs are group contents, so a
step names the page that holds them, not the context directly. And a user may
only interact at the application boundary: the command travels from
context Storefront to the entity, not from the user to the entity. Routing a
user's command straight at an entity is an Error.
User Story Verbs¶
The user-story verb may be any of wants, must, shall, should, may,
will, can. All variants parse to the same structure — the modality is
vocabulary, not captured data.
Interaction Step Kinds¶
| Step | Syntax |
|---|---|
| Focus | step focus <user> on <group> |
| Direct | step direct <user> to <url> |
| Select | step <user> selects <input> |
| Take input | step take <input> from <user> |
| Show output | step show <output> to <user> |
| Refusal | step <source> refuses <user> "<reason>" |
| Send message | step send <message> from <source> to <target> |
| Self-processing | step for <ref> is "<description>" |
| Arbitrary | step from <source> "<relationship>" to <target> |
| Vague | step is "<subject>" "<verb>" "<object>" |
The refusal step is new in 2.0. It models a system element declining a user's request, which previously had no way to be expressed except as free text:
The arbitrary and vague steps carry free prose rather than typed references, so they are the two that the AI-translatability check examines.
Steps may be grouped with sequence { … }, parallel { … } and
optional { … }.
Users interact only at the application boundary
A User may not reach past the application straight into the domain. For the
two untyped step kinds — arbitrary and send-message — when exactly one side
resolves to a User, the other side must be a UI element or a definition
whose enclosing context has the application intention. Otherwise it is an
Error.
Use Case Completeness
RIDDL analyzes whether each use-case step is witnessed by the model's structure, and whether the ordered sequence of steps is an admissible trace through the state machines of the entities it drives. Both emit CompletenessWarnings:
- A
sendstep whose receiver has no matchingonclause, or no reachable wiring from sender to receiver - A
show outputstep with no handler doingput … to <output> - A
take/select inputstep whose input type nothing consumes - A message delivered to an entity in a state that does not handle it
- A step whose free-text prose contains no word found in the in-scope
vocabulary, so it is unlikely to be AI-translatable into a test. Adding
termdefinitions and descriptions enlarges the vocabulary and quiets this.
Modules and Imports¶
A module is a named, flat collection of any top-level definition:
module Commerce is {
domain Shopping is { ??? }
type Money is Decimal(12, 2)
author Reid is { name is "Reid Spencer" email is "reid@ossuminc.com" }
}
Modules can be compiled to a binary .bast file and imported by other models.
Importing¶
The full form takes everything the file's root module holds:
The selective form takes exactly one named definition, optionally renamed:
An importable kind may be any of domain, context, entity, type, epic,
saga, adaptor, function, projector, repository, streamlet,
author, module, user, connector, constant, invariant.
An imported definition must be structurally legal where the directive sits, exactly as if it had been written there by hand. An illegal placement is an Error.
Import vs Include¶
include "file.riddl"splices source text; the parser rules are determined by the enclosing container.import "file.bast"loads compiled definitions from a binary module.
Include Hygiene
- StyleWarning: an included file whose path does not end in
.riddl - MissingWarning: an include that parsed but contributed no definitions
Version¶
A version declares a component of a scope's version coordinate. It is either a
name or a natural number, never both:
domain Garibaldi is {
version Garibaldi
context Ordering is {
version 4
command Reorder is { orderId is UUID }
record OrderState is { status is String }
entity Order is {
version 3
state Open of record OrderState is {
handler OrderHandler is { on command Reorder { ??? } }
}
}
}
}
A definition's precise version is composed from every versioned ancestor,
root to leaf, joined with .. The Order entity above is Garibaldi.4.3. Only
scopes that actually bear a version contribute a component, so an unversioned
context would give Garibaldi.3 — which is what makes adoption incremental.
Versions may be declared at Root, Module, Domain, and all six processor kinds. Declaring more than one version in a single scope is an Error.
A version coordinate is not a semantic version
The composed form looks like semver but is a hierarchical coordinate.
Do not read compatibility into it: 3.1.6 → 3.2.1 says nothing about
breakage, and Garibaldi.4.3 is not orderable against Jellyfish.1.1 at
all. Its length varies with nesting depth, so comparison must be
component-wise. A generator targeting a semver-demanding ecosystem needs an
explicit mapping rule.
Copyright¶
A copyright is a named notice carried verbatim:
domain Shopping is {
copyright House is "© 2026 Ossum Inc."
external context StripePayments is {
copyright Stripe is "© 2026 Stripe, Inc."
}
}
The literal string is the notice in its entirety — symbol, year and holder — because notices vary by jurisdiction, holder and license.
Unlike version, a copyright does not compose. The applicable notice is the
one declared by the definition itself or, failing that, by the nearest
ancestor that declares one. That is the point of allowing it at inner scopes: a
vendored external context bearing a third party's notice must override its
enclosing domain's, not be appended to it.
Copyrights may be declared at the same nine scopes as versions. Declaring more than one in a single scope is an Error.
Metadata¶
Metadata goes in a with { } block after the closing brace of the
definition:
event-sourced entity Product is {
// Entity definition content
???
} with {
briefly as "Product available for purchase"
described by {
| Represents a product in the catalog that customers can purchase.
}
by author Reid
term SKU is {
| Stock Keeping Unit, the unique identifier for a variant
}
}
The available metadata kinds are briefly, described by/described at/
described in file, term, option, by author, figma, attachment and
comments.
Metadata Validation
Style Warnings:
- Repeating a single-valued metadata kind (
briefly,ulid). Only the first is used.author,see,term,option,commentanddescribedmay be repeated freely. - The same
termname defined at two scopes with different definition text. Identical redefinitions are fine.
Missing Warnings:
- A Domain that identifies no author, considering its own author references and defined authors as well as those of any enclosing domain
Figma References¶
A figma reference connects a model element to the exact frame in a Figma file
that depicts it:
application context Storefront is {
page Checkout is { ??? } with {
figma "aBcD1234" node "42:1337"
}
}
The two literal strings are the file key and the node id — exactly the two arguments the Figma REST API takes — so the reference resolves to one frame rather than a document a human must search.
It is metadata, not a definition: it has no identifier, nothing can
path-reference it, and it contributes nothing to the symbol table. It is
permitted on Input, Output, Group, and an application context — the
definitions that describe user interface.
Drift validation is opt-in and off by default
With --check-figma-drift and a FIGMA_TOKEN in the environment, a node
the API does not know about is an Error, and a frame whose name does not
correspond to the annotated definition's name is a Warning. Names are
compared on letters and digits only, so Payment Screen, PaymentScreen
and payment_screen all correspond.
An offline build cannot be affected: the flag is off by default, no token means no client, and every failure to reach or understand the API produces nothing at all. Only a successful API answer can produce a message.
Validation Message Severity¶
RIDDL's validator produces messages at the following severity levels (from lowest to highest):
| Severity | Kind | Description |
|---|---|---|
| 0 | Info | Informational notes |
| 1 | StyleWarning | Naming and style conventions |
| 2 | MissingWarning | Missing optional content |
| 3 | UsageWarning | Unused definitions or unreferenced declarations |
| 3 | Deprecation | Use of a construct slated for removal |
| 4 | CompletenessWarning | Model is valid but incomplete for implementation |
| 5 | Warning | Likely mistakes or problematic patterns |
| 6 | Error | Invalid constructs that prevent compilation |
| 7 | SevereError | Fatal errors that halt processing |
Deprecation messages are rendered with their own [deprecated] label rather
than being folded in with ordinary warnings, so a model can be checked for zero
deprecations independently of zero warnings. They surface under every command —
parse, validate, stats, bastify and the prettify and generation
commands — not only validate.
CompletenessWarning (severity 4) identifies models that parse and validate
correctly but are missing details needed for a complete, implementable
specification. They can be toggled with the -c /
--show-completeness-warnings CLI flag (default: true).
Messages at severity 4 and above are considered actionable — they should be addressed before considering a model ready for translation or code generation.
Best Practices¶
- Include Metadata: Add descriptions to all definitions in
withclauses after their closing braces. - Be Explicit: Always specify reference kinds (entity, command, event…).
- Ascribe Shapes: Write
as <shape>on any processor with ports, so the intent is stated rather than inferred. - Name Constants: Comparisons require typed references, so give thresholds
names —
constant MaxItems is Natural = "100"— rather than embedding magic numbers. - Declare
yields: A command or query that declares its response gives generators a precise signature and lets the validator check conformance. - Mark
initial: Marking the starting state and handler explicitly makes a model safe to reorder. - Refuse First: Put every
requireanderrorahead of every effect. - End When Statements: Always terminate
whenwithend. - Use Match for Multiple Conditions: Prefer
matchover a chain ofwhenstatements. - Model Complete Flows: Include UI components and user interactions, and
keep all UI inside an
applicationcontext. - Follow Containment Rules: Only define elements within their appropriate containers.
- Terminate Every Stream: Every outlet needs a connector; route genuinely
unused output to
BottomlessPit.
Common Parse Errors¶
Some mistakes produce an error that does not point at the thing that is wrong. These are the ones worth recognising by their symptom.
Expected ("(") at a field, far from any obvious cause¶
You have named one of your own types after a parameterized predefined type. The parser reaches the reference, recognises the built-in name, and demands its argument list — so the error is reported at the use, which may be hundreds of lines from the declaration that caused it.
type Currency is any of { USD, EUR, GBP, JPY } // the actual cause
// ...
record Payment is {
amount: Currency // Expected ("(") reported HERE
}
The fix is to rename your type — CurrencyCode, say.
Predefined names whose parentheses are required produce this misleading
form: Currency, Decimal, Pattern, Id. Naming a type after a bare
predefined instead gives a clear, well-located error at the declaration —
"Type 'Location' redefines built-in type 'Location'" — so those are easy.
The full set of reserved type names:
| Group | Names |
|---|---|
| Text | String, Pattern, URL |
| Numeric | Boolean, Integer, Natural, Whole, Number, Real, Decimal |
| Physical | Current, Length, Luminosity, Mass, Mole, Temperature |
| Temporal | Date, Time, DateTime, TimeStamp, Duration, ZonedDate, ZonedDateTime |
| Identity | UUID, UserId, Id |
| Other | Currency, Location, Nothing, Anything, Abstract |
Grep a model for these before you start if you are hunting an error of this shape.
Expected one of ("*" | "+" | "," | …) after a collection field¶
You have written many of X. The collection prefix is many with no
of:
items: many ProductInfo // correct
items: ProductInfo* // equivalent
items: many of ProductInfo // does not parse
of does belong to the other collection forms — sequence of X, set of X,
graph of X, mapping from K to V — which is what makes many of such an
easy slip.
Expected one of ("(" | "yields") at the is of a message¶
The body is empty. RIDDL has no empty body; use the ??? placeholder:
The error lands on is rather than on the braces, because the parser is still
looking for what may follow the identifier.
A comment after ???¶
??? ends the body, so nothing may follow it inside the braces. A comment
may, however, come before it — that is the whole point of the
undefined = {comment} "???" production:
domain MyDomain is {
// Start building your domain model here
??? // fine -- the comment introduces the stub
}
domain MyDomain is {
???
// Start building your domain model here
// does not parse -- ??? already closed the body
}
Both parts survive a prettify round trip. The comment is kept as part of
the container's contents, and the ??? is re-emitted for a body that holds
nothing but comments — because the marker records deliberate intent ("not
specified yet") that a bare comment does not.
Undefined Bodies¶
??? is available wherever a body may be left unspecified, and the same rule
applies at each: an optional run of comments, then the marker. That covers
container bodies (domain, context, entity, processor, repository, adaptor,
projector, group, handler, author), type bodies (aggregate_definitions,
enumerators, alternation_contents), statement blocks, with { } metadata
blocks and doc blocks.
Common Syntax Issues¶
- Don't use
=to assign fields; useset field x to <value>.let x = …is valid for local bindings. - Don't forget
endafterwhenstatements. - Always include reference kinds before identifiers.
state … ofandmorph … withtake a record, not a message.sendtargets an outlet; usetellfor direct delivery to a processor.- A comparison operand must be a reference or a named constant, never a literal.
- Place comments only where definitions are allowed, not within clauses.
- Metadata blocks follow their definitions rather than being nested within them.
- Never place entity, type, or repository definitions directly within a domain — they belong in a context. (A repository is the exception, when it genuinely spans contexts.)
- Never place definitions inside an author — authors only contain metadata.
- Use
whenfor conditionals;ifis not supported. - Use
dofor describing implementation logic, not bare quoted strings. - Every adaptor handler needs an
on otherclause. - Attach at most one connector to any port.
Incomplete Definitions¶
Use ??? as a placeholder for incomplete definitions:
record PaymentDetails is { ??? } with {
briefly as "Record for payment information details"
}
page Checkout is { ??? } with {
briefly as "Checkout page for completing purchase"
}
Author Inheritance¶
Authors are defined once and inherited throughout the model hierarchy, but
cannot contain any definitions. An author definition may only appear in a
Module or a Domain body; everywhere else, reference one with by author Name in
the with { } block:
domain ShopifyCart is {
author Claude is {
name is "Anthropic Claude"
email is "support@anthropic.com"
} with {
briefly as "Model creator and maintainer"
}
context ShoppingContext is {
???
} with {
by author Claude
briefly as "Main shopping context containing commerce entities"
}
} with {
briefly as "Shopping cart domain model"
}
Deprecated Constructs¶
Everything below still parses in 2.x and emits a [deprecated] message. All of
it is slated for removal in 3.0.
| Deprecated | Replacement |
|---|---|
source/sink/flow/merge/split/router keywords |
processor <id> as <shape> |
prompt "..." statement |
do "..." |
Abstract type |
Anything |
state X is record R, or no introducer at all |
state X of record R |
when "a natural-language condition" |
when prompt("…") |
send … to inlet X |
send … to outlet Y, or tell |
| anonymous nebula (top-level definitions with no wrapper) | module <Name> is { … } |
option is gateway/service/external/wrapper |
the context intention prefix |
requires { … } inline aggregation |
requires <TypeRef> |