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

Guides

The UI model

Reactivity, layout and theming as parts of one language. A breakpoint is a branch, a query is a subscription, and a colour is a token with two values.

01

A theme is a declaration, and dark mode is the second half of each line

Not a second stylesheet, and not a class you toggle. A colour is a token with a light value and a dark one, written together — so they cannot drift, and nothing has to be kept in step.

demo/kanban/model/board.osyverbatim — this file compiles
theme Studio {
  Colors {
1    Bg        = Modes.Of(light: "#F6F7F9", dark: "#0E1116");
    OnBg      = Modes.Of(light: "#16181D", dark: "#E6EAF0");
    Surface   = Modes.Of(light: "#FFFFFF", dark: "#171B21");
    Border    = Modes.Of(light: "#E3E6EA", dark: "#2A2F37");
    Subtle    = Modes.Of(light: "#6B7280", dark: "#8A94A6");
2    Accent    = "#4F46E5";
    OnAccent  = "#FFFFFF";
    LaneBg    = Modes.Of(light: "#EEF0F4", dark: "#1C212A");
  }
3  Radius   { Card = "12px"; Control = "8px"; Pill = "999px"; }
  FontSize { Caption = "12px"; Body = "14px"; Section = "16px"; Display = "26px"; }
  FontWeight { Medium = "600"; }
  Length     { LaneW = "300px"; }
4  Breakpoints { Cozy = 900; }
}
1

Both modes on one line. Adding a dark mode to an app is editing the lines that already exist, not writing a parallel set of them.

2

A token with no pair is the same in both. The brand colour usually is.

3

Not just colour. Radii, type sizes, weights and lengths are tokens too, so rounded: Radius.Card is a decision made once.

4

Your breakpoints, named by you. There is no sm/md/lg vocabulary to learn — and see §3, where this name is used as a branch rather than as a media query.

02

Layout is arguments

There is no class attribute, no utility soup, and no stylesheet that has to agree with the markup. Spacing, colour, radius and size are typed arguments on the element, and every value is a token.

demo/kanban/model/board.osyverbatim — this file compiles
1[Composable] component Column<T>(T[] cards, string title, int count) {
  render {
2    Stack(gap: 2, p: 2, w: Length.LaneW, bg: Colors.LaneBg, rounded: Radius.Card) {
      Row(gap: 2, align: Align.Center, px: 1) {
        Text(title, fontSize: FontSize.Section, fontWeight: FontWeight.Medium);
        Text(count, fontSize: FontSize.Caption, color: Colors.Subtle, px: 2, py: 1, bg: Colors.Surface, rounded: Radius.Pill);
      }
      Stack(gap: 2) {
        foreach (var c in cards) {
3          Slot(c);
        }
      }
    }
  }
}

[Composable] component Board<T>(T[] lanes) {
  render {
4    if (Layout.AtLeast(Cozy)) {
5      Row(gap: 3, align: Align.Start, overflowX: "auto", pb: 2, role: UiRole.Region, label: "Board") {
        foreach (var l in lanes) { Slot(l); }
      }
    } else {
      Stack(gap: 3, role: UiRole.Region, label: "Board") {
        foreach (var l in lanes) { Slot(l); }
      }
    }
  }
}
1

Generic over what it holds. A lane knows it stacks things; it does not know what a card looks like.

2

Arguments, checked by the compiler. A typo is an error at the position it happened, not a style that silently does nothing.

3

The caller supplies the child. One lane component serves every board, because the page that uses it decides what goes in.

4

⭐ A breakpoint is a BRANCH. Wide enough and the lanes are a Row; narrower and they are a Stack. Two structures from one source, with no media query and no second template — and because it is ordinary control flow, a phone can render genuinely different markup rather than the same markup pushed around.

5

Accessibility is in the declaration, on the element that is the region. UiRole is a real vocabulary the compiler checks, not a raw string a typo breaks silently.

03

A query is a subscription, and state is just a field

The reactivity model is two words long: a plain field is client state, and a live var stays true. There is no store, no selector, no query key and no invalidation.

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

  on mount { SeedBoard(); }

  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;

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

Server, and live. An entity query runs on the server under your declared security, and live keeps it true — a card somebody else moves lands here with no poll and no refetch.

2

The moves available on the selected card, from the WORKFLOW. Not a list the page maintains: the workflow already knows which transitions are legal and who may take them, so the board asks it.

04

Four apps, and nothing new to learn between them

A personal archive, a chat client, an arcade game and the platform's own control plane — five screens between them. Same 46 controls, same 16 atoms, same tokens, same render block — and no two of them look remotely alike. Flip each one to see what drew it.

A personal archive, set like a book Cream paper, one serif carrying the actual words, colour used once, rules instead of boxes. Nothing here is a kit control wearing a different colour — it is the same kit, with this app's token values.
A personal notes app: an index of notes set in a serif on cream paper
Apps/recall-osy/model/pages/notes.osy verbatim — this file compiles
component NotesPage() {
  live var notes = Note.OrderByDescending(n => n.ModifiedAt).ToList();

  string draft = "";

  action Add() {
    if (draft == "") { return; }
    var created = CreateNote(draft);
    draft = "";
    Navigation.Go("/notes/" + created.Id);
  }

  render {
    Stack {
      ScreenHead(eyebrow: notes.Count == 1 ? "One note" : notes.Count + " notes", title: "Notes");
      Composer(value: draft, placeholder: "New note", onAdd: Add, disabled: draft == "");

      if (notes.Count == 0) {
        Blank(title: "Nothing written down yet",
              body: "A note is the fastest way to stop holding something in your head.");
      }

      CardGrid {
        foreach (var n in notes) {
          RuledRow(title: n.Title, sub: n.Tags == null ? "" : n.Tags, href: "/notes/" + n.Id);
        }
      }
    }
  }
…and one of its notes, open The same app, two screens. The body is a Markdown field rendered by an atom — read-only here, because a note you are only reading should not pay for the editor.
One note from that app, open for reading, its Markdown body set as prose
Apps/recall-osy/model/pages/notes.osy verbatim — this file compiles
    Stack(maxW: Size.Read, mx: "auto", w: "100%") {
      Row(align: Align.Center, pb: 5) {
        Link(href: "/notes") {
          Text("←  Notes", fontFamily: Font.Sans, fontSize: FontSize.Label, letterSpacing: "0.14em",
               textTransform: TextTransform.Uppercase, color: Colors.TextMuted);
        }
        Spacer();
        if (note != null) {
          Link(href: "/notes/" + note.Id + "/edit") {
            Text("Edit", fontFamily: Font.Sans, fontSize: FontSize.Label, fontWeight: FontWeight.Semibold, letterSpacing: "0.14em",
                 textTransform: TextTransform.Uppercase, color: Colors.Primary);
          }
        }
      }

      if (note != null) {
        Stack(gap: 3, pb: 6) {
          if (note.Tags != null && note.Tags != "") { Eyebrow(text: note.Tags); }
          Text(note.Title, fontFamily: Font.Display, fontSize: FontSize.Title, color: Colors.OnBg,
               letterSpacing: "-0.015em", lineHeight: "1.05");
        }
        // The read-only renderer, set as prose: serif, long line height, on the paper rather than in a box. A note
        // you are only READING should not pay for a megabyte of editing machinery.
        Box(fontFamily: Font.Text, fontSize: FontSize.Body, color: Colors.OnBg, lineHeight: "1.7") {
          Markdown(note.Body);
        }
A team chat client, in the dark half of its theme The transcript is a foreach over a query. The dark mode is the same theme — one token declaration with both values, not a second stylesheet.
A team chat client in dark mode, with a channel rail and a transcript
demo/ember/model/pages/channel.osy verbatim — this file compiles
        foreach (var p in history) {
          Row(gap: Space.Beat, align: Align.Start) {
            Avatar(initials: p.Author.DisplayName, size: Size.Sm);
            Stack(gap: "2px", minW: "0") {
              Row(gap: Space.Step, align: Align.Center) {
                Text(p.Author.DisplayName, fontSize: FontSize.Caption, fontWeight: FontWeight.Semibold);
                Text(p.SentAt.ToString("HH:mm"), fontSize: FontSize.Micro, color: Colors.TextSecondary, fontFamily: Font.Mono);
              }
              MessageBody(body: p.Body, handles: handles);
            }
          }
        }

        foreach (var m in arriving) {
          Row(gap: Space.Beat, align: Align.Start) {
            Avatar(initials: m.Author, size: Size.Sm);
            Stack(gap: "2px", minW: "0") {
              Row(gap: Space.Step, align: Align.Center) {
                Text(m.Author, fontSize: FontSize.Caption, fontWeight: FontWeight.Semibold);
                Text(m.SentAt.ToString("HH:mm"), fontSize: FontSize.Micro, color: Colors.TextSecondary, fontFamily: Font.Mono);
              }
              MessageBody(body: m.Body, handles: handles);
            }
          }
        }
A lit 3D valley, at 60 frames a second One Canvas, a camera, a light and a few hundred Draw.Mesh calls a frame. The scene is Osy# — and the whole loop runs in the browser, because that is where its work is.
A low-poly 3D valley rendered on one canvas: a bird flying between green pipes, with sun shadows and fog
demo/arcade/model/pages/birds.osy verbatim — this file compiles
      // A pipe that slid off the left edge respawns at the back of the train — the list never grows.
      foreach (var p in pipes) {
        if (p.X < -11.0) {
          p.X = far + 9.0;
          far = p.X;
          p.GapY = 1.9 + (Roll() % 1000) / 1000.0 * 2.6;
          p.Scored = false;
        }
      }
    }
    scroll = scroll + speed * dt;

    if (state == 0) { birdY = 2.6 + Math.Sin(t * 2.2) * 0.16; }
    if (state == 2) {
      deadFor = deadFor + dt;
      vy = vy - 13.5 * dt;
      birdY = birdY + vy * dt;
      if (birdY < birdR + 0.04) { birdY = birdR + 0.04; vy = 0; }
    }

    // The camera breathes after the bird instead of being bolted to it.
    camY = camY + (2.4 + birdY * 0.25 - camY) * 5.0 * dt;

    DrawSky();
    Draw.Camera(3.4, camY + 1.3, 17.5, 3.4, camY, 0, 42);
    Draw.Light(-0.35, -1.0, 0.45, "#fff1d0", "#a8ccff");       // a late-afternoon sun, front-right and high
    Draw.Fog("#d6ecff", 26, 74);
    DrawValley();
    DrawPipes();
    DrawBird();
    DrawHud();
The platform's own control plane A grid is a control like any other, and a column is a selector — so renaming the field is a compile error here, not a blank column later.
The osyrin control plane listing organizations in a data grid with status pills
admin/model/pages/organizations.osy verbatim — this file compiles
        DataGrid(
            label: "Organizations",
          rows: orgs,
          columns: [
            new GridColumn<Organization> { Name = "Name", Label = "Organization", Value = o => o.Name, Title = true, Fill = true },
            new GridColumn<Organization> { Name = "Slug", Label = "Slug", Value = o => o.Slug, Secondary = true },
            new GridColumn<Organization> { Name = "Type", Label = "Type", Value = o => o.Type.Label, Width = 140 },
            new GridColumn<Organization> { Name = "Status", Label = "Status", Value = o => o.Status.Label, Width = 140 }
          ],
          rowSelected: OpenOrg
        ) {
          // A CELL TEMPLATE (D78): the Status column renders the app's own Osy#, not the grid's text. The pill is the
          // kit's Badge — app code, forkable — and the TONE is a presentation decision about the status, so it lives
          // HERE, in the app's UI layer, never on the enum (metadata must not dictate UI). The label inside the pill
          // is the enum's own [Label], resolved because `o.Status` is a direct read of an enum-typed field.
          slot Status { o =>
            Row(align: Align.Center) {
              if (o.Status == OrganizationStatus.Active) { Badge(tone: Tone.Success) { Text(o.Status); } }
              else { Badge(tone: Tone.Warning) { Text(o.Status); } }
            }
          }

05

What happens when you change it

UI changes, and what they cost

Change a token
compiles
Every use follows, in both modes. That is what a token is for.
Add a dark value to a colour that had none
compiles
Edit the line. There is no second file, and no chance of adding it in one place and not the other.
Use a token the theme does not declare
refused
Refused, by name. A missing token is a compile error rather than a colour that renders as nothing.
Move a breakpoint
compiles
It is a number in the theme, and the branch reads it. Nothing else changes.
Fork a kit control
compiles
osy get ui/field copies it into ui/lib/, where it shadows the kit's by name. It is ordinary Osy# — the same language your app is written in.

Where to go next

One program

Where this page's code runs, and why you did not choose.

UI reference

63 pages — controls, layout, theming, reactivity.

kanban

The app on this page, one command away.