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". - 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¶
Sibling names must be unique, regardless of kind¶
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 Clause¶
A command or query may declare the response it produces. This makes the request/response pairing declarative, so generators can emit precise signatures:
command PlaceOrder yields event OrderPlaced is {
cartId is CartId
}
query GetOrder yields result OrderInfo is {
orderId is OrderId
}
A command's yields must resolve to an event; a query's must resolve to a
result. yields on any other kind of type is an Error.
yields is optional. When it is declared, the handler for that message must
yield exactly that message. When it is not declared, a handler may still
yield whatever it likes.
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 yieldson a type that is not a command or query- A command whose
yieldsnames a non-event, or a query whoseyieldsnames a non-result
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 {
when ord.total > MinimumOrder then
yield event OrderPlaced(id = ord.id)
end
}
}
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 {
yield 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 is defaulted to initial only when the entity has
exactly one state. With multiple states, entity-scope handlers are common parts
merged into each state's handler set, so none of them is the entity's initial
handler.
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.
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(id = x, total) |
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 Statement¶
Produces a command's or query's declared response, without the handler needing to know the sender's identity:
yield satisfies the completeness check that command handlers emit an event
and query handlers produce a result.
reply is deprecated
reply is a deprecated synonym for yield. It parses to the same node and
emits a [deprecated] message.
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.
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 "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 order.status {
case Pending {
tell event OrderPending to entity Order
}
case Shipped when order.isPaid {
tell event OrderShipped to entity Order
}
case >= HighValueThreshold {
tell command EscalateReview to entity Review
}
default {
error "Unknown order status"
}
}
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.
Require Statement¶
Asserts a precondition that must hold before proceeding:
require amount > Zero
require "the customer is in good standing"
require invariant BalanceNonNegative
Validation
Invariants defined in an entity but never referenced by any
require invariant statement produce a UsageWarning.
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 ProcessPayment 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"
}
} 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 {
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 {
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 {
page ProductDetails is { ??? } with {
briefly as "Page showing product information"
}
page ShoppingCart is {
button Checkout activates type Boolean with {
briefly as "Checkout button to proceed to payment"
}
}
page Payment is {
form PaymentEntry submits type PaymentDetails
}
} with {
briefly as "User interface components for the system"
}
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.AddToCartForm from user Customer
step send command AddToCart from user Customer to entity Cart
step show output Storefront.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"
}
}
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
entity Order is {
version 3
}
}
}
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:
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
option is event-sourced
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> |
reply <msg> |
yield <msg> |
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> |