Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

The reactivity & lifecycle model

declarations stay live · on mount (once, before paint) · on change (on every tracked change) · on unmount (once, at teardown)

How an Osy# component comes alive and stays in sync: declarations are live value bindings, `on mount`/`on unmount` are once-only lifecycle bodies, and `on change` is a tracked reaction. A change re-renders ONLY the slots that read it — never the whole page — and a region that leaves disposes its own live queries and reactions automatically.

stable3 examples compiled by CIuiauthoringreactivitylifecycle

Summary#

An Osy# component is reactive by construction. You declare what its values are; the runtime keeps them current and re-renders only what actually changed. There is no re-render call, no dependency array, no manual subscribe/unsubscribe. This guide is the mental model behind component's members — read it once and the rest of the UI reference falls into place.

Four kinds of member make up the model:

MemberRunsFor
live var x = … / a server readcontinuously — a live value binding, recomputed whenever anything it reads changeswhat a value IS
var x = …once, at setup — then it holds whatever it was last assignedstate you assign to
on mount { … }once, before first paintone-time imperative setup
on change { … }at mount, then on every tracked changea reaction — push a value outside the component
on unmount { … }once, at teardown (after children)one-time teardown

"Anything it reads" includes anything a helper it calls reads — WITH ONE EXCEPTION, AND IT IS THE CLOCK. A live var that calls a method or a top-level function tracks the DATA that function read. It does not track a clock read inside it, and the compiler refuses that shape rather than letting it go stale:

WrittenRecomputes when
live var due = items.Where(i => i.At < DateTime.UtcNow).ToList();the clock ticks, or items changes
live var due = items.Where(i => IsDue(i, DateTime.UtcNow)).ToList(); — the instant passed INthe same
live var due = items.Where(IsDue).ToList();bool IsDue(Item i) => i.At < DateTime.UtcNow;REFUSED

THIS TABLE SAID "the same" FOR THE THIRD ROW UNTIL 2026-09-01, AND THE COMPILER HAS NEVER AGREED. Written exactly as that row showed, osy validate answers: "live due reads the clock through IsDue(…), and a clock read inside a function does NOT advance — it is evaluated once… Reactivity is decided where the read is WRITTEN, not where the function is called."And that refusal's own for more pointer names THIS PAGE, so a reader who followed it arrived at the sentence that had just been rejected. Measured on eval run 23 of 012, which wrote the documented form, was refused, and spent four calls re-reading this page trying to reconcile the two. Read the clock where the value lives, or pass the instant in.

Description#

The lifecycle of an instance#

An instance runs this sequence, once:

  1. Setup — declarations initialize. var/live var fields take their initial value; server reads kick off (their rows stream in). A live var is a value BINDING and stays current for the instance's whole life; a plain var is a value you HOLD — its initialiser runs once and it changes only when something assigns to it.

    ⚠ That distinction is the one readers get wrong. A derived value written as a plain var looks right, renders right on the first paint, and then never moves — so reach for live var whenever the value is computed from something that can change, and plain var only for state you assign to yourself.

  2. on mount — once, before the first paint. One-time imperative setup: seed an editable new Entity{} ghost, run a sequence, prime some state. Because it runs before first render, what it sets is already on screen in the first paint. (Data loading is a declaration — a query — never on mount.)

  3. First render — the DOM is built once, and each dynamic slot (a text expression, a prop, an if condition, a foreach source) gets its own tiny reactive binding to exactly the values it reads.

  4. Steady state — a change wakes only what read it. When a value changes, only the slots and on change blocks that read that value re-run — not the whole page. A one-row edit patches one text node; an unrelated field elsewhere is untouched.

  5. on unmount — once, at teardown. Runs when the instance goes away, after its children have torn down. The instance's own live queries and reactions are disposed automatically at the same moment.

Declarations vs. on mount — the one distinction to internalize#

A declaration says what data is — a pure value binding that stays live:

live var open = orders.Where(o => o.Status == Status.Open);   // recomputes when orders (or the filter) change

on mount is one-time imperative work — a new Entity{} you intend to edit (a declaration's new T{} is a plain object, not a committable ghost), a load-time fetch, a startup side effect:

on mount { draft = new Organization {}; }   // a committable ghost in the page overlay, seeded once

Reach for a declaration first; reach for on mount only when the work is imperative and must happen exactly once.

What a live var may be — and why an ordinary server function isn't one#

Which one it is follows from its initializer:

InitializerWhat you getStays current because
an entity readInvoice.Where(…), from Invoice where …a reactive queryit subscribes to data changes and refetches when its dependencies change
a projected readFolder.Select(f => new FolderNode { … })a reactive query of valuessame subscription, but each row is a plain projected shape (see below)
an expression over client valuesdraft?.Name ?? "…", a + ba tracked computedit recomputes synchronously whenever a value it read changes
a stream<T> callTail(path), Ask(question) (yield — a function that produces results over time)a live append-only listthe stream is the subscription — the server holds the connection open and pushes, so there is nothing to poll and nothing to invalidate

A call to an ordinary server function is none of these, so it is a compile error:

live var files = FilesInFolder(selectedId);   // ✗ FilesInFolder runs on the server

Nothing subscribes such a value to data changes, and a tracked computed cannot recompute synchronously — it would have to hand off to the server mid-render. Both ways out are spelled out by the diagnostic:

var files;                                            // ✓ fetch once, imperatively
on mount { files = FilesInFolder(selectedId); }

live var files = FileAsset.Where(f => f.FolderId == selectedId);   // ✓ inline the query — genuinely reactive

And a third, when the answer builds up rather than changing — make the function a stream<T> (yield — a function that produces results over time). That removes the objection rather than working around it: a stream is its own subscription, so each result renders the moment it arrives.

stream<FileInfo> FilesInFolder(Guid id) { … yield return file; … }   // the producer
live var files = FilesInFolder(selectedId);                          // ✓ items appear as they are found

Inlining the query is almost always what you actually wanted: it re-runs when selectedId changes and when the underlying rows change, which is the behaviour the server-function spelling only appeared to offer.

A client-side function is fine — it is a tracked computed like any other expression, so live var greeting = Shout(name) compiles and recomputes when name changes. The rule keys on where the callee runs, not on the fact that it is a call.

A projected live var — a live list of a shape you declared

A reactive query may project into a class (Select (projections)), exactly as a function return may — so a live var can hold a live list of the shape you actually want to render, not the raw entity:

live var nodes = Folder.Select(f => new FolderNode {   // a live list of FolderNode — reshaped, and reactive
  FolderId = f.Id,
  Name     = f.Name,
  ParentId = f.Parent?.Id ?? Guid.Empty
});

It subscribes to the source entity (Folder) just like an entity read, so it refreshes on commit — create or delete a folder and the list updates itself, with nothing to reload. What differs is the rows: a projection has no row identity, so each row is a plain value of your shape rather than a tracked entity. You read its fields (node.Name) and pass it on; there is simply no per-row entity to edit or track through it — the whole result refreshes together when the source data changes. Reach for it wherever you would return a projected class from a server read, but want the result to stay live instead of being fetched once.

Fine-grained updates — why "on change" is tracked, not "every render"#

on change is dependency-tracked: it subscribes to exactly the reactive values its body reads, and re-runs only when one of those changes. That is the whole reason the name is on change and not "effect that runs every render" — a block that re-ran on every render would be waste, and the name forbids that reading. Any reactive read counts, not only a live var: a plain state field reassigned by an action wakes it too. See on change.

Can I hold editable state as class values?#

A class is an in-memory shape with no row behind it, so a component field holding a List<T> of them is an obvious way to carry working state — a set of selections, a draft split, a basket of lines. The question that follows is if I write to one of those objects, does the screen move?, and the answer has two halves that point in opposite directions. Reading one is fully reactive. Binding a control to one is refused. Design against one half alone and you will either write a reassignment you did not need, or an edit that cannot compile.

Reading a class field is tracked like any other read. A render slot that reads p.Weight subscribes to that field, and a write through the object — from an action, through the list, through any alias of it, since a class is a reference type ([[class-index#reference]]) — wakes exactly the slots that read it and nothing else. It is the same fine-grained tracking as everything else on this page; a class value is not a blind spot in it.

So you do NOT need picks = picks.ToList(); after mutating an element. That reassignment is a reflex carried in from frameworks that diff by list identity, and here it buys nothing: the write already re-rendered, and rebuilding the list only makes the runtime redo work it had done. Write in place.

class Pick {
  public string Name = "";
  public int Weight = 1;
  public Pick(string name) { Name = name; }
}

[Page("/picks")]
[AllowAnonymous]
[Render(CSR)]
component Picks() {
  List<Pick> picks = [ new Pick("Ann"), new Pick("Bo") ];

  int Total() { int t = 0; foreach (var p in picks) { t = t + p.Weight; } return t; }
  action Bump(Pick p) { p.Weight = p.Weight + 1; }   // in place — nothing reassigns `picks`

  render {
    Stack(gap: 3) {
      foreach (var p in picks) {
        Row(gap: 2) { Text(p.Name); Text("w=" + p.Weight); Button("Bump " + p.Name, onPress: () => Bump(p)); }
      }
      Text("Total shares: " + Total());
    }
  }
}

[Test]
void mutating_a_class_field_in_a_list_rerenders() {
  Ui.Visit("/picks");
  Assert.Visible("Total shares: 2");
  Ui.Click("Bump Ann");
  Assert.Visible("Total shares: 3");   // the derived total moved, from one field written in place
}

But a control's two-way value: cannot target a class field. A two-way binding has to write back somewhere, and a class value has no row behind it to write through — so rather than hand you a field that accepts typing and saves nothing, the compiler refuses it:

foreach (var p in picks) { NumberField("Weight", value: p.Weight); }   // ✗ refused, at compile time
ERROR  RESOLVE_ERROR  UI: cannot two-way bind to `p.Weight` — `p` is a `class`, and a class value has no row behind
it to write through, so the edit would be read-only. A two-way target is an assignable field of this component, or a
field of an ENTITY. Hold the value in a component field and copy it into the class when you save it, or make the row
a real entity.

Take the first of those two ways out and the whole thing works, because of the half above: bind an ordinary component field, then copy it into the class on save — the copy is an in-place write, so the derived total moves with it.

[Page("/picks/edit")]
[AllowAnonymous]
[Render(CSR)]
component PickEditor() {
  List<Pick> picks = [ new Pick("Ann"), new Pick("Bo") ];

  int draft = 1;      // an assignable component field — this is what the control binds to
  Pick editing;       // which class value the draft is destined for

  int Total() { int t = 0; foreach (var p in picks) { t = t + p.Weight; } return t; }

  action Edit(Pick p) { editing = p; draft = p.Weight; }
  action Save() { editing.Weight = draft; editing = null; }   // the copy — an in-place write, so the screen moves

  render {
    Stack(gap: 3) {
      foreach (var p in picks) {
        Row(gap: 2) { Text(p.Name); Text("w=" + p.Weight); Button("Edit " + p.Name, onPress: () => Edit(p)); }
      }
      if (editing != null) {
        Row(gap: 2) { NumberField("Weight", value: draft); Button("Save", onPress: () => Save()); }
      }
      Text("Total shares: " + Total());
    }
  }
}

[Test]
void a_component_field_carries_the_edit_into_the_class() {
  Ui.Visit("/picks/edit");
  Assert.Visible("Total shares: 2");
  Ui.Click("Edit Ann");
  Ui.Fill("Weight", "4");
  Ui.Click("Save");
  Assert.Visible("Total shares: 5");
}

So pick the shape by how the value is produced, not by how it is stored. A class value is an excellent carrier for anything derived — a computed balance, a settlement transfer, a running subtotal, a projected row — because those are written by your own code and read by the render, which is exactly the half that works. It is the wrong carrier for anything a person edits directly through a control: there, either keep the edited scalar in a component field and copy it across as above, or make the row a real entity and bind to that. Both are ordinary; neither needs the list reassigned.

Automatic disposal — the ease of live without the leak#

Every reactive thing a region creates — a live query's subscription, an on change reaction, a child component — is owned by that region's scope. When the region leaves (a foreach row drops, an if branch flips, the page closes), its scope disposes and takes all of that down with it, children-first. You never write the un-subscribe; forgetting to stop a live-ness is not a bug you can have here.

Examples#

All four kinds on one page — a declaration feeds the render and a reaction; on mount seeds; on unmount closes out:

entity Organization { string Name; }

[Page("/org/new")]
[Render(CSR)]
component OrgCreate() {
  Organization draft;
  on mount { draft = new Organization {}; }          // once, before paint — seed the editable ghost

  live var tabName = draft?.Name ?? "New organization";   // a live declaration — recomputes as you type
  on change { Navigation.SetTitle(tabName); }        // a reaction — re-runs only when tabName changes
  on unmount { Log.Information("create form closed"); }    // once, at teardown

  render {
    Stack(gap: 4) { Input(value: draft.Name, placeholder: "Organization name"); }   // the text slot tracks draft.Name
  }
}

See also#

  • component — the member table this model underlies.
  • on mount / on unmounton mount / on unmount in full.
  • on change — the tracked reaction, in full.
  • creating & saving data — how a seeded new Entity{} ghost rides the page overlay and UnitOfWork.Commit().
  • Classes — what a class value is, and why an edit through a list index sticks ([[class-index#reference]]).

Related

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

on mount / on unmount

`on mount { … }` runs a block ONCE, the first time a component appears — before its first paint; `on unmount { … }`…

on change

`on change { … }` is a reactive **side-effect**: the runtime re-runs it whenever a value it read changes, so it's how…

creating & saving data

A UI `action` creates, updates and deletes data by writing `new Entity { … }`, assigning fields, and calling…

Navigation

The routes the user currently has open, and the verbs that move between them. Read `Navigation.Routes` in a layout to…

Select (projections)

Reshape what a query returns: one column, an anonymous row, or a `class` you declared. The projection becomes the SQL…

yield — a function that produces results over time

A `stream<T>` function produces its results one at a time instead of all at once, and a `live var` bound to one renders…

Classes

A class is an in-memory shape — data plus the behaviour that belongs to it — and it never touches the database. That is…

entity

Declares a persisted type — a table of rows the app stores, queries and secures. Every entity gets an Id and audit…