Model types

Published Language

A Published Language is the contract a bounded context offers the outside world. Not the model, and not the implementation: the messages and data shapes another team writes code against, stated precisely enough that both sides can generate from them.

DomoModeler holds one as a small registry. Schemas are filed by category, each schema has versions, and each version has a specification written in a typed language:

event OrderPlaced {
  type schemaTypeName
  version currentVersion
  timestamp occurredOn
  string orderId
  data.Customer buyer
  int quantity = 1
}

That specification is the source of truth. Everything else here reads it: validation, compatibility checking, source code in nine languages, syncing from a model.

Note: The Schema Registry is available on Pro plans and above. On a Guest or Starter organization the Published Languages section does not appear at all. If an organization drops off Pro, its existing languages stay listed and readable so that nobody's work disappears.

Where a Published Language lives

Expand a Domain card on the dashboard and you'll find a Published Languages section.

Where a language appears inside that card depends on what it describes:

  • A language with exactly one source model is nested under that model, up in the models list, the way a Glossary sits under the Domain Model it came from.
  • A language with no source, or with several, is listed in the Published Languages section itself. Nesting a language that spans every bounded context of a Context Map under any one of them would be a lie.

The section's own ⋯ menu chooses whether it sits above or below the models in the card. The current choice is ticked.

Creating one

Click + in the section header, name it, and optionally pick a source model. Only a Context Map or a Domain Model can be a source; an EventStorming board or a Mind Map has no vocabulary to publish.

Naming a source is what files the language under that model rather than in the section, and it is what enables the Sync button in the editor later. You can add one afterwards.

The row menu

Item What it does
Edit Opens the properties dialog: name, namespace, description.
Export The encrypted archive of the whole language. Not built yet, shown disabled. Not to be confused with source-code export, which is in the editor and does work.
Delete Deletes the language, its schemas and every version. Anything integrating against them stops resolving.

There is deliberately no Share item. Access is inherited from the parent Domain, and distribution (source code today, third-party registries later) is what sharing a Published Language actually means in practice.

The hierarchy

Six levels, the first two of which you already know from the dashboard:

Product
  Domain
    Published Language "Orders"
      Commands
        PlaceOrder
          1.0.0
          1.1.0
      Data
        Customer
          1.0.0
      Events
        OrderPlaced
          1.0.0
          2.0.0
        OrderCancelled
          1.0.0
      Queries
        FindOrder
          1.0.0

A Product holds Domains; a Domain holds models and Published Languages. Both are covered in The dashboard. From the Published Language down, the three levels are the registry's own:

  • Category: one of the six below. Categories with nothing in them are not shown.
  • Schema: a named contract, such as OrderPlaced. It is the thing that has an identity over time.
  • Version: a specification at a semantic version, with its own lifecycle status.

The left pane of the editor is this tree from the Published Language down. Click a level to expand or collapse it; click a version to open its specification.

The Published Language editor with the category, schema, and version hierarchy expanded in the left pane.

The editor

Opening a Published Language gives you the standard Domo editor chrome: click the logo to return to the dashboard, click the title to rename, and presence and save status sit where they do in every other editor.

Four actions sit in the header toolbar:

Action Available when
Properties You can edit. Name, namespace and description.
Sync You can edit and the language has a source model.
Release Packages Always. Naming a set of versions is covered above.
Export Always. Generating files from what you can already read takes nothing away from anyone.

The left pane shows the namespace, then the hierarchy described above. Selecting a version opens its specification for editing on the right.

Adding a schema and its versions

+ on a category adds a schema to it. A schema and its first version, 0.1.0, are created in one step, because a schema with no version says nothing. Typing OrderPlaced under Events generates event OrderPlaced { } for you to fill in rather than leaving an empty box.

The form also carries a description and a Scope of Public or Private.

Note: Scope is recorded but not yet enforced. Marking a schema Private states an intention; it does not currently hide the schema or leave it out of an export.

Creating a schema in a Published Language, with the name, category, scope, and description fields.

+ on an existing schema adds a new version. The version number is prefilled with a patch bump of the highest existing version. It is suggested, never imposed, since whether a change is breaking is your call and not a string's. The specification starts as a copy of the previous version's, retitled to the schema's current name and category: a new version is nearly always an edit of the last one, and starting from an empty box invites a copy-paste that quietly drops something.

Compatibility against that predecessor is checked as you type.

Adding a new version to a schema, with the suggested version number and the previous specification carried forward.

Schema categories

Six, and each maps to one keyword that opens a specification.

Category Keyword What belongs in it
Commands command A request for something to happen.
Events event Something that has happened.
Queries query An ask for information.
Data data A shape referenced by other schemas rather than sent on its own.
Documents document A whole document payload.
Envelopes envelope Metadata wrapping a message.

Documents and Envelopes are never produced by syncing from a model; they are messaging concerns with no Domain Model equivalent. You can still author them by hand.

Writing a specification

The header writes itself

Type a name into the schema form and the specification is generated for you:

event OrderPlaced {
  
}

Change the name or the category afterwards and only the header is rewritten. The body is yours and is preserved exactly as typed, whitespace included. If the first token is something you wrote deliberately rather than a category keyword, it is left alone for you to fix.

Fields, written either way round

Both syntaxes mean exactly the same thing:

string orderId
orderId : string

The space around the colon is optional. Mixing them inside one specification parses, and a specification rewritten from one style into the other is not treated as a change; the comparison is on meaning, not on text, so it will not propose a new version or a sync update for it. That said, pick a lane.

Note: Field and schema names are letters and digits, starting with a letter. No underscores, and no hyphens. Use orderId, not order_id.

Special fields

Three keywords describe the message rather than its payload:

Written Means
type schemaTypeName The fully-qualified schema name, carried in the message itself. Usually a string in the generated code.
version currentVersion The semantic version of this schema version, carried in the message.
timestamp occurredOn When the instance was created. Usually a long or a string.

The name after the keyword is yours; the examples above are conventions, not requirements.

These three are excluded from compatibility checking, because they are envelope metadata rather than the contract being compared. Adding or removing one never counts as a breaking change.

Note: In the colon form you can name a payload field type, version or timestamp; type : string declares an ordinary string field called type. The traditional form cannot express that, because there the word is always the special field.

Primitive types

Type Range or values
boolean true or false
byte 0 to 255
char one UTF-8 character, in single quotes
short −32,768 to 32,767
int −2,147,483,648 to 2,147,483,647
long −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
float single-precision, to ±3.4028235E38
double double-precision, to ±1.7976931348623157E308
string UTF-8 text

Append [] for an array of any of them: string[] tags.

Default values

Follow a field with = and a literal to give it a default. A field with a default is optional; a field without one is required, and that distinction is what compatibility checking uses.

data OrderDefaults {
  boolean planned = true
  byte shortcutValue = 65
  char shortcut = 'A'
  short unreceivedReading = 12986
  int quantity = 1
  long tenYearTotal = 15329885886279
  float approximatePi = 3.14
  double preciselyPi = 3.1416
  string label = "ABC"
}

Array defaults are a comma-separated list in braces:

data OrderSamples {
  boolean[] votes = { true, false, true }
  int[] sampleRange = { 518279, 400131 }
  string[] notes = { "one", "two", "three" }
}

Two rules catch people out:

  • A string literal used as a default must be 1 to 64 characters. That is a limit on the default value, not on what the field can carry at runtime.
  • A numeric literal is range-checked against the field's type, so byte level = 300 is rejected.

Escapes in char and string literals are the usual ones: \t, \n, \r, \b, \f, \", \', \\, plus \uXXXX.

Referring to other schemas

A field's type can be another schema in the same Published Language. So a shape declared once:

data FullName {
  string givenName
  string familyName
}

is referred to from another schema by name:

data ContactInformation {
  FullName fullName
  string emailAddress
}

A bare name like FullName must start with a capital letter and is resolved in the same category, at its most recent version. To reach another category, or to pin a version, qualify it:

Written Means
FullName Same category, latest version
data.FullName The Data category, latest version
data.Telephone[] An array of them
data.Telephone:1.1.0 Pinned to version 1.1.0
data.Telephone:1.1.0[] An array, pinned

Note: A version can only be pinned on the qualified form. Telephone:1.1.0 is not accepted; write data.Telephone:1.1.0.

What a pin does when you export

A pin decides which version of that schema goes into the generated file. Pin data.Customer:1.0.0 and the exported Customer has 1.0.0's fields, even when a newer version exists. It does so even when 1.0.0 is Removed, since a retired version is kept precisely so a pin still resolves.

Because a generated file holds one type per schema, every reference to a schema has to agree on which version that is. The export dialog reports what it chose under Versions selected by a pinned reference, in three cases:

  • A pin selected an older version than the newest one, so the file deliberately does not contain the shape you can see in the editor.
  • A pin names a version that does not exist, so the newest was used instead.
  • Two references disagree: one pins 1.0.0 while another takes the latest. The newest is generated and both referrers are named, because only one of them can be satisfied in a single file. Resolve it by agreeing on a version, or by exporting the two consumers separately.

Comments

// to the end of the line and /* … */ both work anywhere whitespace does.

Validation as you type

The editor parses the specification while you write it and reports three kinds of problem:

  • Syntax: with the line and column, from the grammar itself.
  • Type literals: a default that does not suit its field: wrong kind of literal, out of range, a char literal that is not exactly one character.
  • Compatibility: how this version relates to the one before it. Covered below.

The server checks too. Moving a version from Draft to Published re-parses the specification and refuses the transition if it does not parse or if it is empty. A registry full of Published versions that say nothing is worse than one that made you finish writing them.

Note: Compatibility checking currently runs in the editor only. Syntax and emptiness are enforced on the server; a breaking change is not.

Versions and lifecycle

Semantic versioning

Versions are MAJOR.MINOR.PATCH, and the number is a promise about what changed.

Increment Means
Major A breaking change. Consumers must act.
Minor Something was added. Existing consumers keep working.
Patch Nothing about the interface changed.

Starting at 0.1.0

A schema's first version is 0.1.0, not 1.0.0, and that is deliberate.

Semantic versioning reserves 0.y.z for initial development, where anything may change at any time, and gives 1.0.0 a specific meaning: this is the public contract. A first version is a Draft, which is by definition "not yet something another context should depend on." So numbering it 1.0.0 would have the version claiming a stable contract while its status said the opposite. The version number is the half that travels to a consumer, so it is the half that has to be honest.

While the major version is 0, a breaking change is allowed on a minor bump. Going from 0.1.0 to 0.2.0 you may remove a field, change its type, or make it required, and it is reported as a warning rather than refused. That is what 0.x is for: you are still working out what the contract should be.

Two things still hold below 1.0.0:

  • The minor must move. A breaking change from 0.1.0 to 0.1.1 is refused, and tells you to use 0.2.0. Within 0.1.x a reader can still expect the fields to be there.
  • Nothing else relaxes. Versions must still increase, and the specification must still parse and be non-empty before it can be Published.

When the contract settles, move to 1.0.0. That increment is how you say it is stable, so it is not questioned even when the change itself was only additive; the warning about an unnecessary major bump applies from 1.x upward, never to the graduation out of 0.x.

Note: Schemas created before this change started at 1.0.0 and are unaffected. Only newly created schemas begin at 0.1.0.

Publishing a 0.x version for the first time

The first time you publish a schema (while every version of it is still a Draft), and the version you are publishing is still 0.x, you are asked:

First published version of OrderPlaced

Publishing says this schema is something other teams can depend on. 1.0.0 is how semantic versioning states that; 0.x means still taking shape.

⚠️ Whichever you choose is what consumers pin, and it cannot be changed afterwards; a published version is frozen.

Keep 0.4.0 · Publish as 1.0.0

Choosing Publish as 1.0.0 renumbers the version you are publishing. It does not create a second version: the specification, the description and the version itself are unchanged; only the number differs, and only at this one moment.

Neither answer is the recommended one. Publishing a deliberate 0.x as an explicitly unstable preview is a real choice, and the version number is what a consumer pins, so it is yours to make rather than ours to make for you.

⚠️ This is the only time a version's number can change, and the only time it is offered. A version's number is normally fixed the moment it is saved; you get a new number by adding a new version, never by editing one. The exception holds here for one reason: nothing of this schema has ever been published, so nobody can have pinned any of its numbers yet. A moment later that stops being true, permanently. Closing the dialog without choosing publishes nothing, so you can come back to it.

You will not be asked when:

Situation Why
The version is already 1.0.0 or higher There is nothing to offer.
Any version of the schema has ever left Draft It has had a public life, so its numbers are settled. That includes one that went straight to Deprecated.
The schema already has a 1.0.0 Two versions cannot share a number.
You are moving to Deprecated or Removed Renaming a version while retiring it makes no sense.
Another schema pins this version Renumbering would break its reference. See Referring to other schemas.

Version status

Status moves in one direction only, and the server answers 409 to any attempt to move it backwards.

Status Meaning
Draft Editable. Not yet something another context should depend on.
Published Frozen. Other contexts may integrate against this version.
Deprecated Still resolves, but consumers should move off it.
Removed Retired. Kept so a pinned reference resolves rather than breaking.

Only a Draft can be edited. A Published version is what somebody else pinned, and changing it would silently change the meaning of their integration. To change a Published schema, add a new version.

Nothing is ever deleted by status. A Removed version still resolves, because somewhere a build has it pinned.

Editing a schema version, with the status control showing the statuses it may advance to.

Compatibility rules

When you write a new version, it is compared against the one before it.

Situation Result
The version does not increase Error
A breaking change, same major Error, naming the major to move to
A breaking change, with a major bump Allowed, listed as warnings so none is a surprise
Additive only, with a major bump Warning: a minor would have done, and a major forces every consumer to act for no reason
Additive only, on a patch bump Warning: a minor says "there is something new here"; a patch says the interface did not move
Below 1.0.0: a breaking change, with a minor bump Allowed, as warnings. See Starting at 0.1.0
Below 1.0.0: a breaking change, on a patch bump Error, naming the minor to move to
0.x → 1.0.0, additive only Allowed, and not warned about; declaring stability is what a 1.0.0 is

Breaking means one of three things:

  1. A field was removed.
  2. A field's type changed.
  3. A field went from optional to required; it lost its default, so every producer that omitted it now fails.

Adding a field is additive even without a default. Going the other way, from required to optional, is additive too.

Syncing from a Domain Model or Context Map

If a Published Language has a source model, Sync reads that model and offers to bring the language into line with it.

What becomes a schema

Messages and data shapes cross a boundary. Behavior does not.

Element Becomes
Domain Event An Event
Command A Command
Query, View A Query
Aggregate, Entity, Value Object, Record, Struct, Type, User Role Data
Domain Service, Repository, Factory, Policy, Saga nothing

A Repository or a Policy is a collaborator, not a payload; it has no wire form, so it produces no schema.

From a Context Map, the elements come from a bounded context's behavior model; naming a single context node when the language was created limits sync to that context, and otherwise every context contributes, with the same element appearing in two contexts counting once. From a Domain Model, they are its own elements.

Where the fields come from

The fields come from each element's Specifications property, which is Markdown. Write your field declarations in a fenced code block and sync copies them into the schema.

Emitted once payment clears.

```
string orderId
timestamp occurredOn
int quantity = 1
```

A Domain Event named OrderPlaced then produces:

event OrderPlaced {
  string orderId
  timestamp occurredOn
  int quantity = 1
}

You need not write the event OrderPlaced { header; sync supplies it from the element's name and type. If you do write one, it is used as-is, and a header that disagrees with the element's type is reported rather than silently accepted.

A few things worth knowing, all of them covered in full in Editor basics:

  • Declarations are gathered line by line, from every block. Prose between them reads as a comment, so you can explain each field as you go and still get all of them.
  • Tag what is not a declaration. A ```json sample or a ```bash command is skipped. An untagged block is assumed to be aimed at the model, because that is what a code block in a modeling tool usually is.
  • A bad line does not cost you the good ones. A declaration with a typo is reported by name, quoting the line and the reason, and the fields around it still come across.
  • Write nothing at all and the schema is still created, correctly named and categorized, with an empty body.

Reviewing what sync would do

Nothing is applied without you accepting it, item by item. Three kinds of change are offered:

Candidate What it does
Create A new schema, with the model's declarations, or empty if there were none.
Rename The model's name differs from the schema's. Offered, never assumed. A schema may have been named away from the model on purpose.
Update The model's declarations replace the specification of the schema's highest version, which must be a Draft.

The dialog always reports what it read (for example, "Read 6 elements from Orders CM; 5 can become schemas") so that "nothing to change" can be checked rather than trusted. Those two look identical otherwise and are completely different problems.

The sync review dialog listing candidate changes, each with a checkbox to accept or decline it.

Matching is by an element's logical identity, not its name, which is why renaming an element updates its schema instead of creating a second one.

Declining a change is remembered

A change you decline is not offered again on the next sync. Otherwise every sync would re-propose the same schema you have already decided against, and the review would become something to click past rather than read.

It is never hidden, though. Previously declined changes are collected at the bottom of the dialog under "3 changes you declined earlier (not offered again)", and each has an Offer again button that puts it straight back into the current review, where you can accept it like anything else. A decline you cannot see is a decision you cannot reverse.

⚠️ A decline covers that change, not that schema forever. If the underlying change is different next time, it is offered again, because it is a different decision:

You declined Later the model says Offered again?
Create OrderPlaced (no fields) the element now declares fields Yes: the proposal changed
Rename Order → PurchaseOrder rename Order → SalesOrder Yes: a different rename
Rename Order → PurchaseOrder the same rename No
Update a version's fields the same fields No
Update a version's fields different fields Yes

So declining is safe: it quiets the change you have judged, and never the ones you have not seen yet.

The divergence report

After applying, a second stage lists what sync cannot do anything about. Sync only ever writes to the Published Language; it never edits a model.

Divergence Means
Frozen version The model says something different, but the schema's highest version is Published, Deprecated or Removed. Add a new version if the model is right.
Absent in model A schema has no matching element. Nothing was deleted; a Published version of it may be pinned by somebody's build.
Not a schema type The element became a Policy or a Repository in the model, which produces no schema. The schema was left alone.
Unreadable specification Something in the element's Markdown was evidently a declaration and did not parse. Quoted back, with the reason.

A rename you decline lands here too, which is the honest place for it: the two names now differ on purpose.

While a version is open

If a sync updates the version you have open, the editor follows it when you have typed nothing, and keeps what you have typed when you have. In that second case it tells you the stored version has moved and offers to show it. It will not choose for you.

Release Packages

A Release Package is a named set of exactly one version of each schema. Export targets a package rather than whatever happens to be newest, so a release can be reproduced, named, and handed to someone.

Open Packages in the editor header. A new package starts from the highest non-Removed version of every schema (what an export gives you today), and you change what you need to.

Give it a name that documents it: 2026.Q3, orders-v2, early-access.3. Letters, digits, dots, dashes and underscores, starting with a letter or a digit. Names are sorted naturally, so orders-2 comes before orders-10.

A package may leave a schema out

Set a schema to Not in this release and it is omitted. This matters more than it sounds: publishing a package requires every member to be finished, so without omission a single half-written schema would make the whole Published Language unreleasable.

The other common reasons are a schema with nothing releasable at all, and a schema this particular consumer does not need.

Every reference has to resolve

A package is checked as you build it, and the report is live:

  • A field pinned to data.Customer:1.0.0 requires the package's Customer to be 1.0.0.
  • A field written data.Customer with no pin is satisfied by whatever the package chose. Inside a package an unpinned reference means the package's choice, not the global latest. That is exactly what lets a whole release be held at an older set.
  • A reference to a schema the package omitted is an error. The generated file would not compile.

Open, Published, Deprecated

Status Means
Open Being assembled. Its members may be Drafts and those Drafts keep changing. This is what you hand out for early access.
Published Frozen. The membership can no longer change.
Deprecated Still available, but consumers should move to a newer release.

Status advances only, like a version's.

Note: Open rather than Draft is deliberate. Draft is a version status meaning "not yet something another context should depend on." That is the opposite of what an Open package is for.

Publishing requires every member to be Published or Deprecated. A Draft member is refused and named: publish that version, choose another, or drop the schema from the release. Nothing is done to your versions on your behalf.

There is one exception. A Removed version may be in a published package when another member pins it. That is not a loophole: a Published version is frozen, so a pin inside it can never be edited away, and without the exception a release containing it could never be published again once its dependency retired. The package tells you when this applies.

Once published, a package is frozen; copy it to start the next release from the same selection.

Exporting

Export in the editor offers two shapes, and a Release selector when the language has packages. The selector defaults to Latest: the highest version of every schema, which is what you get when you have no packages at all.

What you get
Source code One file, in one language. Paste it into a project.
Bundle (.zip) The whole release as files: schemas, unpack scripts, and optionally the language bindings too.
Push to a registry Sends the schemas straight to a Schema Registry you have set up.

Source code

One file per export, named after the language. Every language requires a package or namespace, and export is refused until you set one.

Language Namespace field What it becomes
Java Package (required) package com.acme.orders;
C# Namespace (required) namespace Com.Acme.Orders
Go Package name (required) the last segment, orders
Rust Namespace (required) pub mod com_acme_orders
TypeScript Namespace (required) export namespace Com.Acme.Orders
JavaScript Namespace (required) nested const objects
Ruby Namespace (required) nested module Com / Acme / Orders
PHP Namespace (required) namespace Com\Acme\Orders\Events
Python Namespace (required) the package path to put the file at, com/acme/orders/

Note: Ruby, PHP and Python do not strictly need one. A class is legal at the top level in all three, and a Python module is already a namespace. It is required anyway, because a generated file full of top-level classes collides with everything else the application has loaded, and Rails, PSR-4 and Python packages all nest by directory in any case. Python is the one language where the namespace cannot appear in the file as code, so the generator writes the intended package path into the module's docstring instead.

Types in the dynamically-typed languages

Seven of the nine put the schema's types in the code itself. Ruby and JavaScript cannot; neither has type declarations, so the types travel as documentation comments instead:

# @param [Time] occurred_on
# @param [Integer] quantity
def initialize(occurred_on:, quantity: 1)
/**
 * @param {string} id
 * @param {number[]} counts
 */
constructor(id, counts) {

Ruby uses YARD, JavaScript uses JSDoc, and they work the same way: the interpreter ignores them completely, while editors read them for autocomplete and hover; VS Code and RubyMine do this out of the box, and JSDoc is what TypeScript's checkJs reads if you turn it on. Nothing is enforced at runtime.

They are comments rather than a checked type system on purpose. Ruby's alternative is Sorbet, whose sig blocks are method calls; the file will not load without the sorbet-runtime gem installed, and a generated contract should not add a dependency to your application.

⚠️ Ruby's constructor takes keyword arguments; JavaScript's are positional. So in Ruby the field names are part of the call and cannot be transposed, while in JavaScript new OrderPlaced(id, counts) depends on getting the order right. That is what the JSDoc is there to help your editor catch.

PHP and Python declare their types, but enforce them very differently

PHP checks every argument as it is passed. The generated file opens with declare(strict_types=1), so handing a string to a field the schema declares as int raises a TypeError at the call rather than surfacing three layers away. Fields are readonly promoted constructor properties, which means they are set once and the property list cannot drift from the constructor signature.

public function __construct(
    public readonly string $orderId,
    public readonly \Com\Acme\Orders\Data\Customer $buyer,
    public readonly int $quantity = 1,
) {
}

Python's annotations are checked by mypy or pyright, and by nothing at all when the program runs. The generated classes are frozen dataclasses, so a field cannot be reassigned after construction, but a value of the wrong type simply goes in.

@dataclass(frozen=True)
class OrderPlaced:
    order_id: str
    buyer: Data.Customer
    quantity: int = 1

Nothing here depends on pydantic. It would validate at runtime, at the price of putting a package into your project that you did not choose, and a generated contract should be droppable into any Python program.

Two details worth knowing:

  • PHP's array carries no element type. int[] counts declares as a plain array, with the element type in a @param int[] $counts docblock that PHPStan, Psalm and your editor read.
  • Fields with a default value are moved to the end of the constructor in both languages. A required parameter after an optional one is deprecated in PHP, and a Python dataclass refuses to be created at all.

Without a package, export takes the highest non-Removed version of every schema, whatever its status. Not "the Published ones": a language still being drafted would export as nothing at all, which is not useful, and a Removed version is retired by definition. Choose a release instead and the package decides: which versions, and which schemas are in the file at all.

Pushing to a Schema Registry

Instead of downloading anything, DomoModeler can register your schemas directly in a Schema Registry: the service your producers and consumers already resolve schemas through.

The registries you can push to

Registry Formats it accepts What you need before you start
Confluent Cloud Avro, JSON Schema A Schema Registry enabled in your environment
Google Cloud Managed Service for Apache Kafka Avro A schema registry created in your project
Google Cloud Pub/Sub Avro Nothing. Schemas live directly in the project
Azure Schema Registry (Event Hubs) Avro, JSON Schema ⚠️ A schema group, created beforehand
AWS Glue Avro, JSON Schema A Glue registry, created beforehand
Amazon EventBridge ⚠️ JSON Schema only An EventBridge registry, created beforehand

Self-hosted registries (Apicurio, or Confluent Platform on your own network) are not on this list, and that is deliberate. DomoModeler only reaches addresses it can construct itself, which is what stops it being pointed anywhere unexpected. For those, use the Bundle instead: it registers exactly the same schemas, in the same order, under the same names, from inside your own network. Nothing is lost but the button.

Setting one up

Choose Push to a registry, then Manage…. You pick your registry type from a list and fill in a few named fields; there is no box to type a URL into. DomoModeler builds the address from what you enter and shows it back to you, read-only. That is a safety feature: because no field anywhere accepts an address, one cannot be aimed somewhere it should not go.

Give it a name you will recognize; that name is what you pick at export time.

Verify before you push

Verify answers two separate questions, and it is worth reading both lines:

  • Address reachable: a registry really is at that address, and for Azure, that the schema group exists.
  • Credential accepted: the key or token you supplied actually works.

You can get the first without the second. A registry that answers "no" to your credential is still proof the address is right, so the two are reported separately rather than as one tick.

If you supply no credential, the second line is absent. It is unchecked, which is not the same as refused.

Where to find your address and credentials

Credentials are typed at the moment you push, used once, and never stored. You will enter them again next time. That is why there is nowhere in DomoModeler to save them.

Confluent Cloud

Field Where it comes from
Cluster ID, Region, Cloud provider Your endpoint URL: Console → your environment → Stream Governance API → Endpoint. Or run confluent schema-registry cluster describe
API key + secret Console → environment → Stream Governance → API keys, or confluent api-key create --resource lsrc-…

⚠️ Two things catch almost everyone here.

The ID is the one in your endpoint URL, not your cluster ID. describe prints both: a Cluster ID like lsrc-v71onqz and an Endpoint URL like https://psrc-1ryeo07.us-west-2.aws.confluent.cloud. They are different strings, and it is the second you take apart. You cannot work one out from the other.

Your API key must belong to the Schema Registry, not to Kafka. Confluent scopes keys to a resource, and a key made for your Kafka cluster (lkc-…) is rejected here with an unhelpful "unauthorized". Make the key against the Schema Registry cluster (lsrc-…).

Also: an address like pkc-abcde.us-west-2.aws.confluent.cloud:9092 is the Kafka bootstrap server, for producers and consumers. It sits on the same console page and is not a registry address.

Google Cloud: Managed Service for Apache Kafka

Field Where it comes from
Project ID, Location, Schema registry ID Console → Managed Service for Apache Kafka → Schema registries
Access token gcloud auth application-default print-access-token

Google Cloud: Pub/Sub

Field Where it comes from
Project ID Console → Pub/Sub → Schemas
Access token gcloud auth print-access-token

Both Google registries take a single access token rather than a key and secret, so you will see one field rather than two. Tokens last about an hour, which suits entering one per push.

Azure (Event Hubs)

Field Where it comes from
Namespace, Schema group Portal → your Event Hubs namespace → Schema Registry
Access token az account get-access-token --resource https://eventhubs.azure.net --query accessToken -o tsv

⚠️ Create the schema group first. Azure will not create one for you from here, and a missing group is the usual reason a push fails. Verify checks for it, so a green "Address reachable" means the group is there.

⚠️ A valid token is not enough on its own. The identity needs the Schema Registry Contributor role on the namespace. Without it you will see "Address reachable" and "Credential refused" together.

AWS Glue and Amazon EventBridge

Field Where it comes from
Region, Registry name Glue: Console → AWS Glue → Stream schema registries. EventBridge: Console → EventBridge → Schemas → Registries
Access key ID + secret access key IAM → Users → Security credentials, or aws iam create-access-key

Your IAM user needs glue:CreateSchema and glue:RegisterSchemaVersion for Glue, or schemas:CreateSchema and schemas:UpdateSchema for EventBridge.

⚠️ EventBridge accepts JSON Schema only; it is the one registry on this list that cannot take Avro, so the format choice differs there.

What a push actually does

Schemas go up in dependency order, because a registry that supports references has to be told about a dependency before anything can point at it. Every schema is reported by name: registered, or not sent and why. Nothing is silent.

Pushing the same schemas again when nothing has changed is safe, and is not an error; most registries simply recognize that they already have them.

Your semantic version travels with each schema, though where it lands depends on the registry. A breaking change (a new major version) is registered under a new name ending .v2, so consumers of version 1 are undisturbed. See "Version numbers appear in two different forms" below, which explains the same idea for the bundle.

The bundle

A bundle is the whole release as a folder you can commit, unzip into a build, or hand to another team. You do not need a schema registry to find it useful; tracking schemas in git is a first-class reason to use it.

Orders-2026.Q3/
  README.md
  schemas/
    avro/com/acme/orders/event/OrderPlaced.v2_1_0.avsc
    json-schema/com/acme/orders/event/OrderPlaced.v2_1_0.schema.json
  src/
    java/OrdersSchemas.java
    typescript/orders-schemas.ts
  domo-pl-schemas-unpack.sh
  domo-pl-schemas-unpack.ps1
  domo-pl-schemas-unpack.cmd

What it contains

  • Schema files, in Avro, JSON Schema, or both. Each file is self-contained: a schema that refers to another has that other schema embedded, so the file stands alone in a repository.
  • Source code, optionally, in any of the nine languages. Each goes in its own subdirectory, so you can include several.
  • Three unpack scripts, always. See below.

Version numbers appear in two different forms, on purpose

Carries
The filename The full semantic version: OrderPlaced.v2_1_0.avsc
The registry subject The major only: com.acme.orders.event.OrderPlaced.v2
The type name itself Never versioned: com.acme.orders.event.OrderPlaced

A filesystem has no notion of compatibility, so the full version in the path costs nothing and makes a git history readable at a glance. A registry subject is exactly where compatibility is checked, so holding one major per subject is what lets the registry compare 2.0.0 against 2.1.0. A major bump, which is a breaking change, correctly becomes a different subject.

The type name stays fixed because it is what other schemas reference and what the registry is keyed by. Versioning it would break both, and rename the type in every generated language binding.

The unpack scripts

Every bundle carries all three, because whoever unzips it may not be on the same operating system as whoever exported it.

Script For
domo-pl-schemas-unpack.sh macOS, Linux, WSL, git-bash
domo-pl-schemas-unpack.ps1 Windows PowerShell (5.1 or later)
domo-pl-schemas-unpack.cmd Windows batch: runs the PowerShell one for you

They do three things.

Copy the schemas into your build:

./domo-pl-schemas-unpack.sh -o ./src/main/resources/schemas

Copy the generated source into your source tree:

./domo-pl-schemas-unpack.sh --source-out ./src/main/java

It is separate from -o deliberately; schemas belong in a resources directory and source belongs in a source directory. If the bundle holds more than one language, add -l java to say which; with only one, it is inferred and you do not need the option.

Register the schemas with a Schema Registry, in dependency order, so a schema is always registered before anything that references it:

./domo-pl-schemas-unpack.sh --registry https://schema-registry.internal:8081
./domo-pl-schemas-unpack.sh --registry https://psrc-xxxxx.confluent.cloud --user KEY:SECRET

On Windows, use the same options with PowerShell naming:

domo-pl-schemas-unpack.cmd -Out .\schemas
domo-pl-schemas-unpack.cmd -Registry https://schema-registry.internal:8081

Add --dry-run (or -DryRun) to any of them to see what would happen without changing anything. All of them are safe to run twice, which is what makes them usable as a build step.

Every option

The shell script and the PowerShell script take the same options under each platform's own naming, so each row below gives both: .sh: for domo-pl-schemas-unpack.sh, and win: for domo-pl-schemas-unpack.ps1 and the .cmd wrapper, which passes whatever you give it straight through to the .ps1.

Option Takes What it does
.sh: -o, --out
win: -Out
a directory Copies the schema files into that directory, creating it if needed. The schemas/<format>/ prefix is dropped, so you get the files themselves rather than the bundle's folders.
.sh: --source-out
win: -SourceOut
a directory Copies the generated source into that directory. Separate from --out on purpose: schemas belong in a resources directory and source belongs in a source directory, and one option serving both would put .java files under src/main/resources.
.sh: -l, --language
win: -Language
one or more language names Chooses which language's source --source-out copies. Only needed when the bundle holds more than one. With a single language it is inferred. Name several by separating them with a comma and no spaces (-l java,csharp,go), or take everything the bundle has with all. The shell script also accepts the option repeated (-l java -l csharp); PowerShell does not, so the comma is the form worth learning.
.sh: --registry
win: -Registry
a registry URL Registers every schema with that Schema Registry, in dependency order, so a schema is always registered before anything that references it. Posts to <url>/subjects/<subject>/versions.
.sh: --user
win: -User
user:password The registry credential, for a registry that needs one. On Confluent Cloud this is your API key and secret as KEY:SECRET. Only meaningful alongside --registry.
.sh: --dry-run
win: -DryRun
— Prints every copy and every registration it would perform and changes nothing: no files written, no requests sent. Works with all three actions.
.sh: -h, --help
win: (none)
— Prints the usage summary. Shell only. In PowerShell, run the script with no options to get the same summary.

At least one of --out, --source-out or --registry is required. Without one there is no work to do, and the script says so and prints the usage rather than exiting quietly as though it had succeeded.

Naming several languages. One bundle often feeds more than one codebase: a C# service and a Java one reading the same events. Rather than running the script once per language, list them:

# one language
./domo-pl-schemas-unpack.sh --source-out ./generated -l java
# two, comma-separated
./domo-pl-schemas-unpack.sh --source-out ./generated -l csharp,java
# one language
.\domo-pl-schemas-unpack.cmd -SourceOut .\generated -Language java
# two, comma-separated
.\domo-pl-schemas-unpack.cmd -SourceOut .\generated -Language csharp,java

The order you list them in does not matter; csharp,java and java,csharp copy the same two files.

⚠️ Commas, with no spaces. -Language java csharp does not name two languages. PowerShell reads the second word as the next unnamed option rather than as part of the list, and older versions of these scripts silently treated it as the schema output directory. They wrote the schemas into a folder called csharp. The scripts now stop and tell you to use a comma instead, but the comma is what you want either way.

⚠️ Repeating the option works only in the shell script. -l java -l csharp is fine in bash; the PowerShell equivalent -Language java -Language csharp fails with "Cannot bind parameter because parameter 'Language' is specified more than once". That comes from PowerShell itself, before the script runs, so it is not something the script can accept or explain away. The comma form works in all three scripts, which is why it is the one to use.

Use -l all (or -Language all) to take every language in the bundle without naming them.

The three actions combine. They are independent, so one run can do all of it: copy the schemas into your resources, the bindings into your source tree, and register everything:

./domo-pl-schemas-unpack.sh \
  -o ./src/main/resources/schemas \
  --source-out ./src/main/java \
  --registry https://schema-registry.internal:8081

What the exit code means, since a build step is usually judged on it:

Code Meaning
0 Everything asked for succeeded.
1 The work failed partway: a registration was rejected, the bundle has no source at all, it has none for the language you named, or curl / python3 is missing.
2 The command itself was wrong: an unknown option, no action given, or several languages present with none chosen.

--registry needs curl and python3 on the path. The script checks for both before it sends anything, so a missing tool stops it at the start rather than halfway through registering. python3 is only used to JSON-encode the schema document into the request body, and it was chosen over jq because a build machine is far likelier to already have it.

⚠️ Registration stops at the first failure. Schemas go up in dependency order, so continuing past a failed one would try to register schemas whose reference the registry has not accepted. The script prints the subject that failed and the registry's own message, and everything registered before it stays registered. Fixing the cause and re-running is the intended recovery. Re-registering an unchanged schema is not an error.

Note: Your credential is passed straight to the registry and is never printed by the script or written anywhere. If a registration fails, the script reports the schema that failed and the registry's message.

Note: The .cmd wrapper exists so a downloaded .ps1 runs without Windows blocking it. To run the .ps1 directly, Unblock-File it first.

If PowerShell prints errors that have nothing to do with the script

Running the .ps1 yourself can produce a wall of red before the script even starts. Most often it is a CommandNotFoundException naming a tool you were not expecting, with a path like Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 in it.

That is your PowerShell profile, not the unpack script. PowerShell runs that profile before anything else, so whatever it fails to do gets reported first and looks like the script failing. A common example is fnm env --use-on-cd, from Fast Node Manager, in a profile written on a machine where fnm is installed but run on one where it is not.

The script itself usually still succeeds. To see that clearly, skip the profile. That is exactly what the .cmd wrapper does for you, and why it is quiet:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\domo-pl-schemas-unpack.ps1 -SourceOut .\generated -Language csharp,java

That is the .ps1 invoked directly, so it carries the whole incantation: -NoProfile, -ExecutionPolicy Bypass and -File. The .cmd wrapper supplies all three for you, which is why its equivalent above is a single short line.

Use -NoProfile whenever you invoke the .ps1 directly, in a build step especially: a build should not depend on whatever happens to be in the profile of the account it runs under.

A namespace matters more here

Export works without a namespace, but every schema name loses its organizational prefix: event.OrderPlaced rather than com.acme.orders.event.OrderPlaced. That is fine inside your own repository and a problem in a shared registry, where it will collide with any other team publishing an OrderPlaced. The export tells you so rather than refusing; a registry push will refuse.

The namespace is seeded from the language's own, and you can override it for a single export without changing the stored value.

The Export source code dialog with the target language list and the package field.

Properties and the namespace

Properties holds the name, the namespace and a description. The dialog is the same one the dashboard row menu opens, except that the editor hides the name field; you rename from the title in the header there.

The namespace is the coordinate consumers resolve this language by, and it is what generated code declares: Java emits package <namespace>;, C#, Rust and Ruby capitalize its segments, Go takes the last one.

It is validated, because a value like Orders Domain or 123-orders produces code that fails in somebody's build rather than in the editor that accepted it. Dotted segments, each starting with a letter or underscore: com.acme.orders. Leaving it empty is fine; a language may not have chosen one yet.

Collaboration

A Published Language is edited live, like a model. Presence shows who else is in it, and the save status sits in the header. Two people editing different schemas never collide; two people editing the same Draft see each other's work.

Not here yet

Encrypted export and import of a whole language The Export item in the dashboard row menu. A Published Language is not a model, so it does not inherit that archive format.
Export to a third-party registry AWS EventBridge and the Azure Schema Registry are the target pair. This is how a Published Language will reach a consumer's runtime.
A public API There is no HTTP surface for retrieving schemas or creating versions from a build pipeline.
Server-side compatibility checking The editor asks before a breaking change; the server does not re-check it.
Remembered sync declines A candidate you decline is offered again on the next run. It shows in the divergence list meanwhile.
Multiple sync sources A language may record several source models, but sync reads the first.