Skip to content

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:

riddlc validate model.riddl 2>&1 | grep -E '\[error\]|\[deprecated\]'

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:

when count > 5 then ??? end          // does not parse

Name the threshold instead:

constant MaxItems is Natural = "100"
when cart.itemCount > MaxItems then ??? end

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:

on other { error "Unrecognized message" }

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:

  1. 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 source streamlet.
  2. send targets 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.
  3. One connector per port. Fan-out is modelled by declaring more ports, not by hanging more connectors off one.
  4. 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, send to 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:

connector DiscardDiagnostics is
  from outlet MyProcessor.Diagnostics
  to inlet BottomlessPit.hole

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 statementsforeach (bounded iteration), put (publish to a UI output), return (a function's result).
  • version and copyright — 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 .bast files.
  • The standard moduleBottomlessPit and ForeverEmpty, always in scope.
  • Structured match — type cases, comparison patterns and guards, instead of quoted labels.
  • On-clause message bindingon ord: command PlaceOrder { … ord.total … }.
  • on activate / on passivate — entity rehydration and eviction, distinct from once-ever on init / on term.
  • Modal user-story verbsmust, shall, should, may, will, can alongside wants.
  • Refusal steps — model a system element declining a user's request.

5. Tooling notes

  • .bast files 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 info reports 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

  1. 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.
  2. Run riddlc prettify to clear most deprecations mechanically.
  3. Fix the deprecations prettify cannot — chiefly the context intentions, which replace the old gateway/service/external/wrapper options. Leave send … to inlet alone for now; it belongs to the next step.
  4. 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 inlet into either a connector or a tell, 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.
  5. 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.
  6. 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.