Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne program

Samples

markdown-demo

Markdown demo — the built-in markdown control, running against real sectioned storage.

5 source files1 test file

The markdown-demo sample, running: The editor over a signed-in user's notes — special relativity, with the Lorentz factor, the transformation and E = mc² as rendered maths; the outline rail lists every section
The editor over a signed-in user's notes — special relativity, with the Lorentz factor, the transformation and E = mc² as rendered maths; the outline rail lists every section

Get it

$ osy init markdown-demo
$ osy launch

The app

app.osy8 lines
// Markdown demo — the built-in markdown control, running against real sectioned storage.
app MarkdownDemo {
  use Osysharp.Markdown@1;

  model "model/**/*.osy";
  tests "tests/**/*.test.osy";
}
model/auth.osy66 lines
// Minimal auth, because the DATA path never inherits anonymity.
[Role] enum AppRole { Authenticator }

entity RoleGrant {
  User Grantee;
  // No `[Required]`: a bare `AppRole` is already required by its spelling (`AppRole?` is the optional one), so the
  // attribute would carry nothing but the platform's own wording for a field no person ever types.
  AppRole Level;
  security { }
}

policy IsAuthenticator => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Authenticator);

[Principal] entity User {
  [MaxLength(200), Unique] string Email;
  [MaxLength(200)] string PasswordHash;
  Markdown Notes;
  security {
    deny read PasswordHash when !IsAuthenticator;
    allow read when IsAnonymous || IsAuthenticated;
    allow create when IsAuthenticator;
    allow update when IsAuthenticator;
    // ⚠ NOT optional for an editor over `me.Notes`. A `Markdown` property is stored as a document the platform
    //   governs by THIS entity's rules — owner `update` is what confers every write on it — so without a self-row
    //   grant a signed-in user could open the editor, type, and have every save refused as "Create of
    //   'Osysharp.MarkdownDocument' denied". Measured 2026-09-04 through the app's own MCP `markdown_import`.
    allow update where Id == user.Id;
  }
}

// ⛔ THE LENGTH CHECK IS NOT CEREMONY. Hashing strength does not answer a weak password: an attacker does not need
//    to break the KDF when the first hundred guesses cover a real share of the accounts. Without this line `""` and
//    `"a"` are accepted passwords — `security-signup-no-password-policy` (SHOULD) says so.
[AuthMethod]
string Signup(string email, string password) {
  if (password.Length < 8) { return ""; }
  var u = new User { Email = email, PasswordHash = Security.HashPassword(password) };
  return Security.IssueJwt(u.Id, u.Email);
}

[AuthMethod]
string Login(string email, string password) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  if (u == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); }
  return "";
}

// `app.AuthBootstrap` names the FUNCTIONS the app's own login page calls; `app.Auth` names the FIELDS, which is
// what anything outside the app needs — `osy user add`, the console login, `osy run --as`. Both, or an operator
// cannot create an account on a deployment that has no admin screen.
app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };

// Audit redaction is EXPLICIT: a credential not named here is written to the entity-change trail verbatim, and
// that trail is durable and exportable. The `deny read` above governs READERS; the audit writer is not one.
app.Audit = new AuditConfig {
  Redact = new AuditRedaction { Properties = [User.PasswordHash] },
};

app.AuthBootstrap = new AuthBootstrap {
  Role      = AppRole.Authenticator,
  Login     = Login,
  Signup    = Signup,
  LoginPage = LoginPage,
};
model/theme.osy32 lines
// markdown-demo — ITS OWN LOOK, and deliberately so: a prose tool reads in a serif.
//
// ⚑ Not the shared demo theme (`demo/_shared/theme.osy`), and `DemoSharedThemeTests` records why.
//   The demos are not meant to look uniform — several of them exist to show that a different
//   palette and a different face are a few lines of `theme`.
//
// ⚠ This restates the ORIGINAL intent with the mechanism corrected. The original spelled it
//   `Fonts { Body / Heading / Ui }` — a group name no control reads — so the Hedvig this app
//   shipped and pinned never reached a single control. `Sans` shadows the KIT's own token, which
//   is what makes a face apply without touching a page.

theme Doc {
  Colors {
    Bg        = Modes.Of(light: "#FAFAF8", dark: "#0E1116");   // paper, very slightly warm
    OnBg      = Modes.Of(light: "#16181D", dark: "#E6EAF0");
    Border    = Modes.Of(light: "#E6E4DE", dark: "#2A2F37");
    Muted     = Modes.Of(light: "#F0EEE8", dark: "#232833");
    Surface   = Modes.Of(light: "#FFFFFF", dark: "#171B21");
    OnSurface = Modes.Of(light: "#1A1F26", dark: "#E6EAF0");
    Primary   = "#4F46E5";
    OnPrimary = "#FFFFFF";
    Danger    = "#DC2626";
  }

  Radius { Sm = "6px"; Md = "10px"; Lg = "16px"; }

  Font {
    Sans = "\"Hedvig Letters Serif\", ui-serif, Charter, Georgia, Cambria, serif";
    Mono = "ui-monospace, SFMono-Regular, Menlo, monospace";
  }
}
model/pages/editor.osy86 lines
// The one page: the markdown control over a real `Markdown` property.

// `MarkdownEditor` and `MenuEntry` come from the kit the manifest opted into. An ordinary import, exactly like
// `using Osysharp.Ui;` — the app declares the DEPENDENCY once in `app.osy` and imports the names where it uses them.
using Osysharp.Markdown;

// ⚑ THE TWO `Input`s IN THE FIND BAR ARE ATOMS ON PURPOSE, AND THAT IS WHAT THE SUPPRESSION RECORDS. `Field`
//   renders its label above the box, which is right on a form and wrong in a one-line toolbar sitting over the
//   document — the `label:` is still there and is still what a screen reader announces, which is the accessibility
//   half the rule exists for. Recorded with the attribute rather than a comment so the linter knows it was a
//   decision; `osy docs ui-atom-where-the-kit-has-a-control` is the rule.
[Page("/")]
[Render(CSR)]
[Title("Markdown")]
[SuppressWarning("ui-atom-where-the-kit-has-a-control")]
component EditorPage() {
  live var me = User.FirstOrDefault();

  bool finding = false;
  string query = "";
  string replacement = "";
  int hit = 0;
  int matches = 0;

  action Find(string selection) {
    finding = true;
    if (selection != "") { query = selection; }
  }
  action Matches(int current, int total) { hit = current; matches = total; }
  action ShowFind() { finding = true; }
  action HideFind() { finding = false; query = ""; }

  string note = "";
  action Archive() { note = "archived"; }

  // ⛔ WHAT THE PAGE LOOKS LIKE WHILE `me` IS STILL ON ITS WAY. Without this the page renders NOTHING until the
  //    server read answers, and a blank screen is indistinguishable from one that hung — which is how it gets
  //    reported. It draws the SHAPE, never the data: it cannot read `me`, which is the whole point of it.
  skeleton {
    Stack(gap: 3, p: 3) {
      Skeleton(shape: SkeletonShape.Control);
      Skeleton();
      Skeleton();
      Skeleton(shape: SkeletonShape.HalfLine);
    }
  }

  render {
    MarkdownEditor(
      ownerType: "User",
      ownerId: me.Id,
      property: "Notes",
      face: editorial,
      outline: true,
      findQuery: query,
      replaceWith: replacement,
      findRequested: Find,
      matchesChanged: Matches,
      extraItems: [
        new MenuEntry { Label = "Archive this doc", Run = Archive },
        new MenuEntry { Label = "Delete block", Run = MarkdownEditor.DeleteBlock },
        new MenuEntry { Label = "Make it a heading", Run = () => MarkdownEditor.TurnIntoHeading(2) }
      ]
    ) {
      slot Toolbar { c =>
        Row(gap: 1) {
          if (!finding) {
            Osysharp.Button("Find", onClick: ShowFind);
          } else {
            Input(value: query, label: "Find", placeholder: "Find", onEnter: c.FindNext);
            Text(matches == 0 ? (query == "" ? "" : "none") : hit + "/" + matches);
            Osysharp.Button("‹", onClick: c.FindPrev);
            Osysharp.Button("›", onClick: c.FindNext);
            Input(value: replacement, label: "Replace with", placeholder: "Replace with");
            Osysharp.Button("Replace", onClick: c.ReplaceOne);
            Osysharp.Button("All", onClick: c.ReplaceAll);
            Osysharp.Button("✕", onClick: c.ClearFind);
            Osysharp.Button("Close", onClick: HideFind);
          }
          Text(note);
        }
      }
    }
  }
}
model/pages/login.osy24 lines
// Sign-in, prefilled. This is a demo whose point is the editor, not the credential flow, so the fields carry
// working values and signing in is one click. `Signup` doubles as "create it if it isn't there yet".
[Page("/login")]
[AllowAnonymous]
[Render(CSR)]
[Title("Sign in")]
component LoginPage() {
  string email = "demo@local";
  string password = "demo1234";

  action SignIn() { Session.SignIn(Login(email, password)); }
  action Register() { Session.SignIn(Signup(email, password)); }

  render {
    Stack(gap: 3) {
      Text("Markdown demo");
      Field("Email", value: email, placeholder: "[email protected]", type: "email");
      Field("Password", value: password, type: "password", hint: "At least eight characters.");
      Osysharp.Button("Sign in", onClick: SignIn);
      Osysharp.Button("Create this account", onClick: Register);
    }
  }
}