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

Samples

wf-nightly-digest

wf-nightly-digest — the SCHEDULED trigger, end to end.

6 source files1 test file

The wf-nightly-digest sample, running.
Running, signed in, compiled from the source below.

Get it

// not one of the apps the toolchain ships: this one lives in the
// repository. Clone it, then:
$ cd demo/wf-nightly-digest
$ osy launch

The app

app.osy6 lines
// wf-nightly-digest — the SCHEDULED trigger, end to end.
app WfNightlyDigest {
  model "model/**/*.osy";
  tests "tests/**/*.test.osy";
}
model/digest.osy45 lines
// The nightly digest itself — a workflow whose runs nobody starts.
enum DigestStatus { Pending, Sent, Empty }

/// One night's digest. A row of this IS an occurrence — the schedule creates it, and everything below reacts to it.
entity DigestRun {
  /// The run's state. No default: the workflow's `Initial` is this field's value from the moment the row exists.
  DigestStatus State;
  /// How many events the digest covered. Zero is a real answer and gets its own terminal.
  int Covered;
  /// When the digest actually went out — the thing an operator looks at to answer "did last night run?".
  DateTime? SentAt;

  security { allow read, create, update, delete when IsAuthenticated; }
}

/// Something worth telling people about. Ordinary app data; the digest sweeps whatever is unsent.
entity Notice {
  [Required, MaxLength(200)] string Text;
  bool Sent;

  security { allow read, create, update, delete when IsAuthenticated; }
}

workflow NightlyDigest {
  Tracks    = DigestRun.State;
  Autostart = true;          // ← the seam. The schedule creates the ROW; this is what turns a row into a run.
  Initial   = Pending;

  state Pending {
    enter {
      var unsent = Notice.Where(n => !n.Sent).ToList();
      this.Item.Covered = unsent.Count;

      if (unsent.Count == 0) { goto Empty; }

      foreach (var n in unsent) { n.Sent = true; }
      this.Item.SentAt = DateTime.UtcNow;
      goto Sent;
    }
  }

  terminal success Sent  { }
  terminal success Empty { }
}
model/identity.osy57 lines
// The identity tier, self-contained. The same shape every other workflow demo uses — a [Principal] account, a [Role]
// enum, and a RoleGrant table recognised by its SHAPE — declared here rather than shared, so this demo compiles on
// its own the way a downloader's copy of it would.
[Principal] entity Account {
  [Required, MaxLength(200)] string Name;
  [Required, MaxLength(200), Unique] string Email;
  [MaxLength(200)] string PasswordHash;
  security {
    deny read PasswordHash when !IsAuthenticator;
    allow read when IsAuthenticated;
    // …and by the AUTH FLOW, which is not "authenticated": the armed principal bears the Authenticator role and has
    // NO user, so `IsAuthenticated` is false for it. Without this line `Login`'s lookup matched no rows and a CORRECT
    // password was refused exactly like a wrong one, while signing UP kept working — which is what hid it.
    allow read when IsAuthenticator;
    allow create when IsAuthenticator;
    allow update, delete when IsAdmin;
  }
}

[Role] enum AppRole { Authenticator, Member, Admin }

entity RoleGrant {
  [Required] Account Grantee;
  [Required] AppRole Level = AppRole.Member;
  security {
    allow read when IsAuthenticated;
    allow create when IsAuthenticator;               // …the signup, for the first grant
    allow create, update, delete when IsAdmin;       // all three verbs, or the weakest is the way in
  }
}

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

[AuthMethod]
string Signup(string email, string password) {
  var a = new Account { Name = email, Email = email, PasswordHash = Security.HashPassword(password) };
  var grant = new RoleGrant { Grantee = a, Level = AppRole.Member };
  return Security.IssueJwt(a.Id, a.Email);
}

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

app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };

app.AuthBootstrap = new AuthBootstrap {
  Role   = AppRole.Authenticator,
  Login  = Login,
  Signup = Signup,
};
model/scheduling_security.osy5 lines
// `Schedule` and its children are BASELINE-shaped and APP-OWNED: the platform ships what a schedule IS and drives the
// rows, and this app owns them — so under the security model it has to say out loud who may reach them. Nothing is
// readable or writable until it does.
partial entity Schedule { security { allow read, create, update, delete when IsAuthenticated; } }
model/seed.osy27 lines
// The schedule — the only thing in this app that makes the digest nightly.
void SeedNightlyDigest() {
  if (Osyrin.Scheduling.Schedule.Any()) { return; }   // idempotent — a second call mints no second schedule

  var nightly = new Osyrin.Scheduling.Schedule {
    Name          = "Nightly digest",
    Template      = new DigestRun { },
    Zone          = "Europe/Stockholm",
    EffectiveFrom = DateTime.UtcNow,
    Overlap       = Osyrin.Scheduling.ScheduleOverlap.Skip
  };

  var everyNight = new Osyrin.Scheduling.ScheduleRule {
    Schedule = nightly,
    Every    = Osyrin.Scheduling.ScheduleFrequency.Day,
    Interval = 1
  };
  new Osyrin.Scheduling.ScheduleRuleTime { Rule = everyNight, At = TimeSpan.FromHours(2) };

}

/// A couple of notices, so a first digest has something to be a digest OF.
void SeedNotices() {
  new Notice { Text = "Quarterly maintenance window moved to Sunday", Sent = false };
  new Notice { Text = "New export format available", Sent = false };
}
model/theme.osy73 lines
// THE SHARED DEMO THEME — canonical copy. Every demo listed in `DemoSharedThemeTests` holds a
// byte-identical `model/theme.osy`, and that test is what keeps them identical.
//
// ⚑ EVERY COLOUR HERE SHADOWS A TOKEN THE KIT ALREADY DECLARES, and that is the whole point.
//   The themes this replaced declared a PARALLEL palette — `Surface0/1/2`, `TextPrimary`,
//   `FillAccent/Success/Warning/Danger` — names `Osyrin.Ui` has never heard of. So the kit's own
//   `Primary`, `Success`, `Warning` and `Danger` stayed at their defaults in six demos: the moment
//   one of them reached for a kit control, that control painted itself indigo next to the demo's
//   blue. `FillAccent` was declared in all six and referenced by NONE — an accent colour that
//   existed only in the theme file.
//
// So the rule for a demo theme is: SHADOW a kit token, never invent a second name for it. A name
// the kit does not have (`Radius.Card`) is a real extension and fine; a second spelling of one it
// does have (`TextPrimary` for `OnBg`) is how an app ends up with two of everything.
//
// Excluded by design — these five own their look and must NOT adopt this file: arcade, ember,
// motion, gestures (each demonstrates a visual world) and Apps/recall-osy.

theme Demo {

  Colors {
    // A deep petrol blue, deliberately not the kit's indigo (#4F46E5) — a demo should look like a
    // considered app rather than an unstyled one, and the two should be distinguishable on sight.
    // It is the one saturated colour on the page; everything else is warm neutral, which is what
    // keeps "distinct" from turning into "loud".
    Primary   = Modes.Of(light: Palette.From("#125E7A"), dark: Palette.From("#57B8D6"));
    OnPrimary = Modes.Of(light: "#FFFFFF", dark: "#06181F");

    Bg      = Modes.Of(light: "#FAF9F7", dark: "#101317");   // the page — a touch warm, so cards read as lifted
    Surface = Modes.Of(light: "#FFFFFF", dark: "#171C22");   // a card, a row, a lane
    Muted   = Modes.Of(light: "#EFEEEA", dark: "#1E242B");   // an inline notice, a disabled field
    Border  = Modes.Of(light: "#E4E2DC", dark: "#28303A");

    OnBg          = Modes.Of(light: "#14181D", dark: "#E7EBF0");
    OnSurface     = Modes.Of(light: "#14181D", dark: "#E7EBF0");
    TextSecondary = Modes.Of(light: "#59626D", dark: "#9BA6B3");   // labels, timestamps, captions
    TextMuted     = Modes.Of(light: "#7C848E", dark: "#7E8894");   // the quietest text on the page

    Success = Modes.Of(light: Palette.From("#1D7A4C"), dark: Palette.From("#4EC98A"));
    Warning = Modes.Of(light: Palette.From("#B26A00"), dark: Palette.From("#E0A343"));
    Danger  = Modes.Of(light: Palette.From("#C1352B"), dark: Palette.From("#F0736A"));
  }

  // Names the kit does not have, so these are extensions rather than second spellings.
  Radius { Control = "8px"; Card = "12px"; }

  // ⚑ THE FONTS ARE WHAT MAKE THIS READ AS A DESIGNED APP RATHER THAN A DEFAULT ONE, and `Sans`
  //   shadows the kit's own token — so every kit control picks it up with no page edit at all.
  //   Both faces are vendored per app under `model/fonts/` and pinned in `osyrin.lock` (`osy font
  //   add`), because a webfont a page merely NAMES is a webfont that silently falls back.
    //   Only the faces the app SHIPS are named here — Mono is left at the kit default because no
  //   demo renders code, and naming an unshipped family is a silent fallback.
  //   Geist for the UI: a neutral grotesque with real character at small sizes, which is where a
  //   dense LOB screen lives. Fraunces for display: a warm variable serif, used ONLY on a page
  //   title. The pairing is the whole look — one voice for reading, one for announcing.
  Font {
    Sans    = "Geist, ui-sans-serif, -apple-system, \"Segoe UI\", Roboto, system-ui, sans-serif";
    Serif   = "Fraunces, \"Iowan Old Style\", Palatino, Georgia, ui-serif, serif";
  }

  FontSize   { Caption = "12px"; Body = "14px"; Subhead = "17px"; Title = "26px"; }
  FontWeight { Regular = 400; Medium = 500; Semibold = 600; }

  // ── APP EXTENSIONS ─────────────────────────────────────────────────────────────────────────────
  // Everything ABOVE this line is the shared theme, held byte-identical across the demos by
  // `DemoSharedThemeTests`. An app may add tokens BELOW it — a name the kit does not have. It may
  // NOT add a second spelling of one the kit already has; that is the defect this file replaced.
  // ── APP EXTENSIONS ─────────────────────────────────────────────────────────────────────────────
  // Everything ABOVE this line is the shared theme, held byte-identical across the demos by
  // `DemoSharedThemeTests`. An app may add tokens BELOW it — a name the kit does not have. It may
  // NOT add a second spelling of one the kit already has; that is the defect this file replaced.
}