Summary#
Prints what your app is — not what its source says, but what the compiler makes of it. Types are bound, relations are wired to the entity they actually point at, every entity reports the access posture it really has, and every function reports what it does when you call it. It needs no platform and no database: it parses and resolves, nothing else.
Signature#
osy model [path] [--json]Description#
Reading source tells you what a field is called and what its type is spelled. osy model tells you what those
resolve to — which is where the surprises live:
Relations are wired. A
LineItem[] Linesfield reports its target: a collection ofLineItem. AOrder Orderfield on the child reports a reference back. You never have to infer a relation from a name.Enum-backed fields are named by their enum, not by the string or int they store as — because the enum is what you have to write.
The rules a field is held to are listed.
[Unique],[Min],[Max],[Pattern],[MaxLength],[Immutable]and friends are reported per field underconstraints, each named as you write it, with its argument. This is how you tell a rule about the data from a check in one screen: a constraint holds wherever the row is written — a function, an import, another tab — while anifin a page holds only in that page. A[Unique(A, B)]written on the entity is a rule about the combination and belongs to no single field, so it is reported on the entity instead. ([Required]is not repeated in that list — it has its ownrequiredflag.)Security is the DERIVED posture, not the block. An entity with no
security { }block is denied to everyone (see secure by default (deny-all)) — not open, and there is no setting that could make it open. That is the fact people most often read backwards, so the model reports the derived posture (deny-all,allow,deny) rather than leaving you to work it out from whether a block is present.Functions report their effects. What each one reads, creates, modifies, deletes, calls out to, raises, and throws — derived from the body, not from the name. Each function is reported twice: its own effects, and its transitive ones (what happens once its callees are included). A function that writes nothing itself but calls one that does will tell you so.
A workflow reports the machine, not just its parts. States and events on their own describe a workflow the way a cast list describes a play. The model also reports what moves between the states, and — because this is where the surprises live — the things that quietly decide an outcome: which states accrue SLA time (a state left out of
Accruespauses every clock while a run sits in it), the schedule those clocks run on, each slot'sCandidatesandRequiresgates, and each milestone's promise, reminders and retry policy.One link is worth knowing about. You raise an event, but a state subscribes it under a slot alias, and the alias is what handlers and transitions use — so
RespondandFirstReplycan be the same act under two names. Each event reports where it can be delivered, and each transition reports the event that drives it, so you never have to reconcile two lists of identifiers by hand.The REST surface you publish is reported, with the columns it hands out. Every
RestApiinapp.Apisappears underapis: the address it is really served on (/api/rest/v{major}/{route}— with the default version applied, so it is where the API is, not what the source spelled), the credential kinds it accepts, each function you mapped to a URL, and each entity you exposed. This is the one part of your app that faces strangers, so it is reported at the level of detail that decides whether something leaks:- No declared
Authmeans anonymous, and the model says so in words rather than by an empty list. An unauthenticated API is the whole story about that API, and it is the easiest thing in the document to read past. - A
Crud<T>selects no subset. It publishes T's whole row — your fields and the ones the platform adds — so the model lists them by name underfields. A cost column or a private note is on the wire, and the declaration that put it there never mentions it. - A field-scoped
deny readis the one thing that takes a column back off, so those are listed separately undermaskedFields. A field that appears infieldsand not inmaskedFieldsis handed to every caller the API admits.
- No declared
The tables you did not write are reported too, separately.
entitiesis what your source declares.providedis what your app has without declaring it: everything ausingbrought in, plus the platform's always-applied core baseline. They are kept apart because "what did I write" and "what tables does this app have" are different questions — but a file-manager app whose whole subject isFileAssetwas reporting five entities and mentioning none of them. Each provided type names theusingit came from, and is flaggedinternalwhen the app cannot name it at all (so nopartial entitycan state security for it). Read one with Reading a capability's source.
The model says when it does not know. If the source did not fully resolve, resolved is false and the model is
partial — it is not presented as the whole truth. If a function contains something the effect analysis could not
classify, it reports unknown: true alongside whatever it did find, rather than quietly reading as "no effects".
--json writes the whole model as JSON — the form to hand to a coding agent, or to diff between two revisions of an
app. Exit is non-zero when the model is partial.
For the shorter question "what names exist here?", osy symbols lists them. For "where does this app fall short of
production?", see Checking your app.
Examples#
osy model # the resolved model, human-readable
osy model --json # the same model as JSONA workflow's slot answers "how do I drive this?" — the event to raise, who may raise it, and what must be true first:
{ "event": "Resolve", "alias": "Fix",
"candidates": "u => u.Team == Team.Support",
"requires": [ { "name": "RootCause",
"must": "!Text.IsEmpty(this.Item.RootCause)",
"message": "Record the root cause before resolving." } ] }What a published API hands out — the address, the credential kinds, and the columns an exposure puts on the wire:
{ "name": "Public", "route": "public", "version": "2.0",
"basePath": "/api/rest/v2/public",
"auth": { "methods": ["apiKey"] },
"expose": [ { "entity": "Order", "operations": ["read", "create"],
"url": "/api/rest/v2/public/entities/Order",
"fields": ["Reference", "Cost", "PrivateNotes", "Id", "CreatedAt", "ModifiedAt", "CreatedBy", "ModifiedBy"],
"maskedFields": ["Cost"] } ],
"endpoints": [ { "function": "Restock", "method": "POST", "path": "/restock",
"url": "/api/rest/v2/public/restock", "successStatus": 201 } ] }A function's effects read like this — PlaceOrder writes nothing itself, but calling it does:
function PlaceOrder(string code) → void
reads Customer · creates Order · calls out to Shipping.CreateShipmentSee also#
Checking your app — check the app against production best-practice rules.
Explaining your app's security — the same app's access rules, in plain English (who can do what).
Compiling your app — compile the source into your local app.
secure by default (deny-all) — why an entity with no security block grants nothing.
- Reading a capability's source — read the source of a capability the model says you were provided