Migrating from RIDDL 1.x to 2.0¶
The one page that spans both versions
Everywhere else, this documentation describes exactly one version of RIDDL. This page is the deliberate exception: it exists for readers who have a working 1.x model and need to know what to change.
If you are learning RIDDL rather than upgrading, skip it — start with Concepts.
Your goal is zero errors and zero deprecations. RIDDL 2.0 gives deprecations their own label, so you can check for them separately:
Work in the order below. The breaking changes must be fixed before the model will parse at all; everything after that can be done incrementally.
Where the time actually goes
Most of this migration is mechanical. One part is not: reworking the streaming topology. In 2.0 every processor is a streaming processor, and connections between them are checked rather than assumed — so message flow that a 1.x model left implicit now has to be stated. Plan for that separately from the rest, and see the suggested order for where it fits.
1. Breaking changes¶
These are hard errors. Fix them first.
A record is data, not a message¶
RIDDL 1.x accepted a record reference wherever a message reference was
expected. It no longer does. There are exactly four messages — command,
event, query, result — and only those can be sent, told, yielded or
handled.
| 1.x | 2.0 |
|---|---|
state S of Foo |
state S of record Foo |
morph E to state S with command C |
morph E to state S with record R |
send/tell/on with a record ref |
rework to use a message |
A state's data type must now be a record. The type it referenced was almost
certainly aggregate-shaped already, so usually you redefine it:
// 1.x
type OrderData is { id: OrderId, total: Currency(USD) }
state Active of OrderData
// 2.0
record OrderData is { id: OrderId, total: Currency(USD) }
state Active of record OrderData
morph carries the new state's record, not the command that triggered the
transition:
// 1.x
morph entity Order to state Shipped with command ShipOrder
// 2.0
morph entity Order to state Shipped with record ShippedData(ship.trackingNumber)
UI requires an application context¶
A group — or any of its aliases such as page,
form or dialog — may now only appear in a context declared with the
application intention. In 1.x, any context containing groups was treated as
an application by convention.
// 1.x
context StoreFront is { page Home is { ??? } }
// 2.0
application context StoreFront is { page Home is { ??? } }
Users interact only at the application boundary¶
In an arbitrary or send-message interaction step where exactly one side is a
user, the other side must be a UI element or a
definition inside an application context. A user reaching straight into a
domain entity is now an Error. See
UI Modeling.
Comparisons are reference-only¶
If you used comparison operators in a condition, note that 2.0's boolean expressions require typed references on both sides. A literal operand fails at parse time:
Name the threshold instead:
This only affects models that had already adopted structured conditions.
A natural-language when "..." still parses — but see the deprecations table:
it should become when prompt("...").
Refusals must precede effects¶
Within a single statement list, every require and error must come before
every set, morph, become, send, tell, yield and put. Acting and
then refusing would leave partial changes behind.
// 2.0 — reorder if your handler did these the other way round
on cmd: command Withdraw {
require cmd.amount > Zero
set field balance to "balance - amount"
yield event Withdrawn(amount = cmd.amount)
}
Adaptors need an on other clause¶
Every adaptor handler must say explicitly what it does with messages it does not recognize:
Adaptors are also now confined to their isolation seam — trafficking in a third context's messages (neither the parent's nor the referent's) is an Error. Types defined at domain or root level are shared vocabulary and are never flagged.
Projectors are event-only¶
on command, on query and on record are rejected inside a projector at
parse time. Queries against projected data are served by the
repository the projector updates.
Functions must be pure¶
A function body may no longer set, morph, become, send, tell or
yield. This is enforced at parse time. Refusals and pure computation stay
legal; use return to produce the result.
Sibling names must be unique regardless of kind¶
type Thing beside entity Thing in one container used to pass with zero
errors, because uniqueness was checked per kind. It is now an Error.
Worth grepping for: with both defined, a reference like Id(Thing) had two
plausible referents and the model never said which.
Definition keywords may not be bare identifiers¶
Eleven keywords introduce a definition — domain, context, entity,
adaptor, saga, epic, projector, repository, streamlet, handler,
function — and none may now be used as a plain name. handler projector is
{ … } is an Error.
The escape is the quoted identifier, so existing names need not be changed:
handler 'projector' is { ??? } // fine
handler Projector is { ??? } // also fine -- the check is case-sensitive
as type T in a schema is strict¶
A repository schema's of <name> as type <T> now requires T to genuinely be
a Type. This check previously never ran, and turning it on produced 202 errors
across 186 of 187 riddl-models — so expect it to fire, and expect the fix to
be pointing at the record rather than the entity that owns it.
An alternation must offer a real choice¶
one of { } is now an Error and one of { A } draws a deprecation. Use
one of { ??? } for undecided.
Repository scope¶
A domain-scoped repository that reaches only one context is an Error — it is provably unnecessary there, so move it into that context. The inverse, a context-scoped repository reaching another context, is only a CompletenessWarning suggesting promotion.
The streaming rules tighten¶
Several new errors govern ports and connectors. They are listed here for completeness, but do not try to fix them one at a time — they interact, and patching them individually tends to produce a topology nobody designed. Section 3 covers them as one piece of work.
- Exactly one connector may attach to any port
- A connector whose ends are in different domains is an Error
- A domain-scoped connector whose ends share one context is over-scoped
- A context-scoped connector whose ends cross contexts is under-scoped
- An ascribed shape that contradicts the declared port arity is an Error
2. Deprecations¶
These still parse and emit [deprecated]. Fix them at your own pace; they are
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 |
Anything |
state X is record R, or no introducer |
state X of record R |
when "a natural-language condition" |
when prompt("…") |
send … to inlet X |
send … to outlet Y, or tell |
| anonymous nebula | module <Name> is { … } |
option is gateway/service/external/wrapper |
the context intention prefix |
requires { … } inline aggregation |
requires <TypeRef> |
option is package |
option is namespace |
Prettify does much of this for you
riddlc prettify normalizes the deprecated shape keywords to
processor … as …, rewrites prompt to do, reply to yield, and
emits Anything for Abstract. Running it over a 1.x model performs a
good part of this table mechanically.
Two of these are worth a note beyond the table.
send … to inlet versus tell. A processor emits on its own outlet
and a connector routes the message to a downstream inlet. Reaching directly
into another processor's inlet bypasses that model — that is what tell is
for. So the fix depends on intent: if you meant streaming, give the sender an
outlet and wire a connector; if you meant direct delivery, use tell.
option is external on a context. The intention prefix replaces it, and
the prefix does more than rename: an external context is now checked for
modeling persistence it cannot own.
3. Reworking the streaming topology¶
This is the largest piece of work in most migrations, and the one that cannot be done by chasing error messages. Budget real time for it.
In RIDDL 1.x, streaming was something a few dedicated streamlets did, and message flow between everything else was mostly implied. In 2.0 every processor is a streaming processor — a context, entity, adaptor, projector and repository may all declare ports — and the connections between them are checked. Flow that used to be assumed now has to be stated.
Four changes combine into that:
- Ports live on every processor. An entity can own an outlet, which is
how it publishes its events. In 1.x an outlet had to live on a separate
sourcestreamlet. sendtargets your own outlet. A processor emits on a port it owns, and a connector routes it onward.send … to inlet— reaching into somebody else's port — is deprecated.- One connector per port. Fan-out is modelled by declaring more ports, not by hanging more connectors off one.
- Connector placement follows what it joins. A connector within one context belongs to that context; one that crosses contexts belongs to the enclosing domain. Crossing a domain boundary is an error outright.
Do it in this order¶
First, decide what each processor emits and consumes, and give it the
ports to say so. This is design work, not editing. An entity that produced
events through a placeholder source streamlet in 1.x almost certainly wants
that outlet on the entity itself now.
// 1.x: the outlet had to live somewhere else
source ProductFeed is { outlet Events is event ProductCreated }
entity Product is {
state Active of record ProductInfo is {
handler Main is {
on command CreateProduct { send event ProductCreated to outlet ProductFeed.Events }
}
}
}
// 2.0: the entity owns the port it emits on
entity Product is {
outlet Events is event ProductCreated
initial state Active of record ProductInfo is {
handler Main is {
on command CreateProduct { send event ProductCreated to outlet Events }
}
}
}
Second, resolve every send … to inlet. This is the deprecation that
needs a decision rather than a rewrite, and it is why the streaming work
belongs before the general deprecation sweep. Ask what was meant:
- streaming — give the sender an outlet,
sendto that, and wire a connector to the receiving inlet - direct delivery — use
tell, which addresses a processor rather than a port
Third, replace connector fan-out with port fan-out. Where several
connectors hung off one outlet, declare an outlet per destination. The arity
is what derives the split shape, so the model then states the fan-out rather
than implying it.
Fourth, place each connector by what it joins. Cross-context connectors move up to the enclosing domain. Expect this to surface genuine domain-analysis problems: a connector whose two ends turn out to be in different domains is an error precisely because it means the boundaries are wrong, and the fix is to rethink the boundary rather than to move the connector.
Give cross-context connectors the persistent option unless you have a reason
not to — without it they draw a CompletenessWarning, because durability at a
context boundary is often model correctness rather than a deployment detail.
Fifth, terminate every stream. Every outlet needs a connector and every inlet needs a feed, so dangling ports now surface as completeness warnings. For output you genuinely do not consume, say so explicitly:
BottomlessPit and ForeverEmpty come from the
standard module and need no import. They
exist for exactly this: under one-connector-per-port, a stream with no real
consumer would otherwise force you to invent a placeholder that models
something untrue.
Finally, ascribe the shapes¶
Once the ports are right, add as <shape> to each processor. The shape is
derived from arity, so an ascription that disagrees is an Error — which makes
it a useful check on the work above rather than mere decoration.
processor OrderEnricher as flow is {
inlet RawOrders is type OrderEvent
outlet EnrichedOrders is type EnrichedOrderEvent
}
Omitting it is only a suppressible StyleWarning, but stating the shape is how the model records what you intended, rather than leaving a reader to count ports.
4. What is new and optional¶
None of this is required. All of it is worth knowing about.
yields— declare a command's or query's response, and the validator checks conformance. Adoptable one message at a time.initial— mark the starting state and handler explicitly, so reordering them cannot silently change behavior.- Value expressions — constructors,
get from,call function,prompt(...), and boolean expressions, replacing opaque quoted strings where structure is useful. - New statements —
foreach(bounded iteration),put(publish to a UI output),return(a function's result). versionandcopyright— scope-wide leaf definitions. A version composes root-to-leaf into a coordinate; a copyright is inherited nearest-first.- Figma references — link a UI definition to one specific design frame, with optional drift checking.
- Modules and imports — package definitions for
reuse and load them from compiled
.bastfiles. - The standard module —
BottomlessPitandForeverEmpty, always in scope. - Structured
match— type cases, comparison patterns and guards, instead of quoted labels. - On-clause message binding —
on ord: command PlaceOrder { … ord.total … }. on activate/on passivate— entity rehydration and eviction, distinct from once-everon init/on term.- Modal user-story verbs —
must,shall,should,may,will,canalongsidewants. - Refusal steps — model a system element declining a user's request.
5. Tooling notes¶
.bastfiles must be regenerated. The binary format is version 2, and its revision has moved during the release-candidate series. A file written by an older build is rejected with a message saying so; there is no migration path for the bytes, so regenerate rather than convert.riddlc inforeports the source git commit, so you can identify exactly which compiler produced a given message.- Deprecations surface under every command that parses, not only
validate. In 1.x a successful parse that accumulated any message discarded the result, so parse-time warnings never reached you. - Spurious option warnings are gone. 1.x maintained two option tables by hand and they drifted, so several valid options drew "not a recognized RIDDL option". 2.0 derives the lists from one registry, and eight previously-unregistered options — including six entity markers — are now recognized.
6. Suggested order¶
- Fix the breaking changes in section 1 until the model parses and validates. Where a streaming error blocks you, do the minimum to get past it — the real work comes at step 4, and a considered fix now would only be redone then.
- Run
riddlc prettifyto clear most deprecations mechanically. - Fix the deprecations prettify cannot — chiefly the context intentions,
which replace the old
gateway/service/external/wrapperoptions. Leavesend … to inletalone for now; it belongs to the next step. - Rework the streaming topology (section 3).
This is the substantial one. Decide what each processor emits and consumes,
give it the ports to say so, resolve every
send … to inletinto either a connector or atell, replace connector fan-out with port fan-out, place each connector by what it joins, terminate every stream, and ascribe the shapes. Expect this to raise genuine design questions rather than mechanical edits. - Re-run until
grep -E '\[error\]|\[deprecated\]'is empty, then read the completeness warnings — under the new streaming rules those are where unterminated streams and unreachable targets show up. - Adopt the new features where they earn their keep (section 4).
Steps 1–3 are mostly mechanical and can be done in an afternoon. Step 4 is where the time goes, and it is worth doing deliberately: the streaming rules exist to make message flow explicit, so the questions they raise are usually questions the 1.x model had left unanswered.