Osy#betaRun it on Osyrin: osyrin.com

Why Osy# · Chapter 10 · One program

The whole application is one program.

An Osy# program holds the application's data, its rules, its behaviour, its screens and its long-running work. The compiler reads those parts together, so it can resolve how they refer to one another, check that they agree, and work out where each part executes. The nine chapters before this one each looked at a part of the programming model. This one is about the fact that those parts are in the same program.

$ osy model --json   # in demo/kanban · abridged"summary": { "entities": 4, "workflows": 1, "components": 5, … }

"Card"       "security": { "posture": "deny",
                           "rules": [ read, create, update, delete ] }
"BoardPage"  "members": [ "lanes": query · server, live,
                            "picked": state · client,
                            "moves": query · server, live, … ]
"AddCard"    "effects": { "reads": [Lane], "creates": [Card] },
             "transitive": { "startsWorkflows": ["CardFlow"] }
In practiceData, rules, behaviour, screens and long-running work are constructs in one program, and the compiler reasons across them.

01

One program contains the parts that belong together

A kanban board, complete: a manifest, a login, two entities with their rules, the workflow that drives a card's review, two pages, the functions they call, a theme, and the tests. The files are organisation for the person reading them. The compiler reads them as one application model, so a name in one file means what another file declares it to mean: the second page asks CardFlow, declared in board.osy, where a card may go.

filewhat is in it
app.osythe manifest: the app's name and the capabilities it uses
model/auth.osythe roles, the user and its rules, sign-up and login, and the login page
model/board.osythe theme, two entities and their rules, the workflow, the board page, and the functions it calls
model/states.osya second page: the same cards, arranged by the state the workflow says they are in
tests/board.test.osy
tests/security.test.osy
the tests, run against the same program

02

One component can use different parts of that program

The board page, which chapter 08 read as a component. Read it again for what each line touches: lanes is a live query over two entities; moved and picked are local state; moves asks the workflow; SeedBoard(), TakeMove() and AddCard() are the application's own functions. The colour beside a line is where the compiler resolved it to execute, from what it touches.

  runs in the browser  runs on the server

demo/kanban/model/board/board.osyverbatim — this file compiles
component BoardPage() {
1  live var lanes = Lane.Include(l => l.Items).OrderBy(l => l.Ordinal).Take(50);

2  on mount { SeedBoard(); }

3  string moved = "";

  action Move(Card card, Lane from) {
    var next = lanes.Where(l => l.Ordinal > from.Ordinal).FirstOrDefault();
    if (next == null) { return; }
    moved = card.Title + " → " + next.Name;
    card.Lane = next;
  }

  action Save() { UnitOfWork.Commit(); }

  Card? picked;

4  live var moves = CardFlow.For(picked).Transitions;

  action Pick(Card card) { picked = card; }

  // ⚠ `picked` IS OPTIONAL — nothing is picked when the page opens — so both actions say so rather than reading
  //   a title off nothing. The render only offers them once a card is picked; that is a guard in the RENDER, and
  //   an action is reachable by a keyboard, a test and a restored page state that the render never gated.
  action Take(Osysharp.Workflow.TransitionView m) {
    if (picked == null) { return; }
5    TakeMove(picked, m);
    moved = picked.Title + " → " + m.Target;
  }

  action Sign() {
    if (picked == null) { return; }
    picked.Signed = true;
    UnitOfWork.Commit();
  }

  string newTitle = "";
6  action Add() { AddCard(newTitle); newTitle = ""; }
1

A live expression over entities. The query executes on the server, under the rules Lane and Card declare, and its value stays current in the component.

2

The lifecycle hook is local; the call is not. SeedBoard() writes entities, so the call crosses to where the data is.

3

Local component state, so the compiler resolves it to the browser.

4

The workflow, asked from the page. The moves the picked card's workflow offers, as a live query: when the card changes, the answer does.

5

The chosen move goes to a function declared further down the same file, which raises it on the card's workflow.

6

Creates a card, so it executes on the server. §4 shows what else it sets in motion.

The component says what it does. The compiler resolves where each part runs.

03

The data, its rules and its workflow are part of the same program

The entities the page queried, with the rules that decide who may touch them, and the workflow it asked. The rule is part of the entity, so every query over Lane anywhere in the program is compiled under it, the live query in §2 included. That is chapter 05, paying off. The workflow is bound to a field of the entity, so the state a card is in is a value the page can show and the workflow can move.

demo/kanban/model/board/board.osyverbatim — this file compiles
entity Lane {
  [MaxLength(40)] string Name;
  int Ordinal;
1  [ForeignKey(Lane)] Card[] Items;
2  security { allow read, create, update, delete when IsAuthenticated; }
}

entity Card {
  [MaxLength(80)] string Title;
  [MaxLength(40)] string Owner;
  Lane Lane;
3  CardReview Review;
  bool Signed;
  security { allow read, create, update, delete when IsAuthenticated; }
}

enum CardReview { Drafting, [Label("In review")] InReview, Approved }

workflow CardFlow {
4  Tracks    = Card.Review;
5  Autostart = true;
  Initial   = Drafting;

  event Submit();
  event Approve();
  event Reject(string why);      // takes an argument, so a one-click move cannot complete it — `NeedsInput` says so

  state Drafting {
    subscribe Submit();
    on Submit { goto InReview; }
  }

  state InReview {
    subscribe Approve();
    subscribe Reject(string why);
6    on Approve { when (this.Item.Signed) { goto Approved; } }
    on Reject  { goto Drafting; }
  }

  terminal success Approved { }
}
1

A collection. The cards on a lane are a property of the lane; the page's Include(l => l.Items) names it.

2

The rule is part of the entity. It governs every read and write of a lane, from any page, function or test.

3

The field the workflow tracks: a card's review state is an ordinary value on the row.

4

The workflow is bound to the entity. Its states are the members of CardReview, and a move writes that field.

5

Every new card starts one. Nothing that creates a card has to remember to.

6

A guard over the row. The page's Sign() sets Signed; until it does, the move to Approved does not happen.

demo/kanban/model/board/board.osyverbatim — this file compiles
void TakeMove(Card card, Osysharp.Workflow.TransitionView move) {
1  Workflow.Raise(card, move);
}

void AddCard(string title) {
  if (title == "") { return; }
2  var first = Lane.OrderBy(l => l.Ordinal).First();
3  var card = new Card { Title = title, Owner = "You", Lane = first };
4  UnitOfWork.Commit();
}
1

The move the page chose is raised on the card's workflow, which takes it from the state the card is in.

2

LINQ over the entities, compiled to one statement. It is the same expression language the page used, because it is the same program.

3

Constructing the entity creates the row in the current unit of work.

4

The unit of work commits once, at the end. Everything above it either happened or did not.

04

The compiler resolves where execution belongs

Execution location is a property the compiler derives from the program, and the resolved model is a command away. It is the same resolved program chapter 07's editor reads.

$ osy model --json      # in demo/kanban · abridged

"name": "BoardPage", "route": "/",
"members": [
  { "name": "lanes",    "kind": "query", "side": "server", "live": true  },
  { "name": "moved",    "kind": "state", "side": "client", "live": false },
  { "name": "picked",   "kind": "state", "side": "client", "live": false },
  { "name": "moves",    "kind": "query", "side": "server", "live": true  },
  { "name": "newTitle", "kind": "state", "side": "client", "live": false }
]

"name": "AddCard",
"effects":    { "reads": ["Lane"], "creates": ["Card"], …, "writesData": true, … },
"transitive": { …, "startsWorkflows": ["CardFlow"], … }

moved, picked and newTitle are component state, so they are client. lanes is an entity query and moves asks the workflow, so both are server, and live. AddCard reads and creates entities, so it is a server function. And it never names the workflow, yet the compiler reports that it starts one: it creates a Card, and CardFlow starts on every card. These are facts the compiler derives from the program, and osy model exposes the resolved result. Captured 2026-10-16.

Execution location, and what a line sets in motion, are properties the compiler derives from the program.

05

Those relationships are what one program buys you

The chapters before this one each showed one of these relationships from its own side. Here are five of them at once, as the compiler holds them: each one exists because both of its ends are declared in the same program.

Security rulegovernsEntity queryfeedsLive componentchapters 05 · 08
Agent, a principalcallsDeclared toolsread and write underEntity securitychapter 02
Test, as a personrunsThe applicationunderThe same ruleschapter 06
WorkflowcallsApplication functionschangeEntitieschapters 03 · 04
Compilerresolves acrossThe whole programand answersEditor, agent, deploychapters 01 · 07

These parts know about each other, because they are declared in one program.

Security can govern a query, a test can run as a person, an agent can be bound by the same rule, and a screen can stay current with the data, because those relationships are inside the program.

06

The same model runs a real application

The board is small enough to read in one sitting. The platform's own control plane, the application that registers, deploys and operates every other application, is written the same way, and the compiler counted it when this page was built.

admin/ · counted by the compiler on this build
252 entities · 3,529 functions · 196 components (67 pages) · 38 workflows
1,896 tests in 223 files · 153,458 lines of Osy#, model and tests together

The same entities, rules, queries, components, functions, workflows and tests as the board, in one program, resolved by the same compiler.

The osyrin control plane listing organizations in a data grid with status pills
The control plane's organizations page, an ordinary Osy# component over an ordinary entity.

What happens when you change it

Change one part. The compiler knows what depends on it.

Each of these is a thing you would do on a Tuesday. The verdict is the compiler's, not a convention's.

You do thisVerdictBecause
Rename Card.TitlerefusedThe page reads card.Title, so the stale read is a compile error, named, until it is updated.
Tighten the security rule on LanecompilesThe live query in §2 is governed by the new rule, along with every other read of the entity in the program.
Add a field the page showscompilesDeclare it on the entity and read it in the render block. One compile carries it to both.
Read an entity from inside renderrefusedRefused, by name. Rendering is synchronous and a server read is not, so the compiler says so at compile time.
Bind live var to an ordinary server functionrefusedRefused: nothing would keep that value current. A query subscribes and a stream<T> pushes; either can be live.
Change what AddCard touchescompilesIts effects, its side and the workflows it starts are resolved again from the new body, and osy model reports them.

Why any of this matters

This is why Osy# is a language

Data, rules, behaviour, screens and long-running work are different parts of an application. In Osy# they are different constructs in the same program, and that is what lets one compiler reason across them: check a reference from a screen to a column, compile a rule into every query, run a test as a person, place each line where it belongs. The syntax being shared is the smaller half. The relationships live inside the program, where a compiler can see them.