Osy#betaRun it on Osyrin: osyrin.com

What's new

What changed, version by version.

Every published version of the toolchain, newest first, with the notes its release carries. The same notes in your terminal: osy whats-new. Install or update with curl -fsSL https://osysharp.com/get | sh.

osy 0.17.8 27 October 2026

  • Fixes and reliability improvements.

The release on GitHub, with every asset and its SHA-256

osy 0.17.7 27 October 2026

Language

  • Counters on existing rows. Add a [Counter] to an entity that already has rows and deploy: the deploy numbers the rows that are there, oldest first, and the counter goes on after them. A migration in the same deploy that creates rows takes the next numbers, where it used to fail the deploy. Docs
  • Counters in any file, or in a kit. [Counter(X)] finds a counter declared anywhere in the app. A counter of your app and one of a kit with the same name are two sequences, and yours is the one a bare name means. Docs
  • Casts over a list in hand. items.Where(x => x is Arc && ((Arc)x).Saga == g) over an in-memory list is no longer refused as SQL.

The release on GitHub, with every asset and its SHA-256

osy 0.17.6 27 October 2026

Language

  • as inside a query. RoleGrant.Select(g => (g.Member as User).Email) used to be refused at run time. It now reads the member wherever the row is a User.
  • Policies in a component's body. A policy named in on mount, an action or a method of a component, as in if (IsPlatformAdmin) { … }, used to compile but fail when the page loaded. It now works.
  • Outlet(retain:) takes a constant. retain: accepts a literal, arithmetic, or a const of the layout, such as Outlet(retain: OpenTabs * 2). A value known only at run time is refused, and the message says why. A const may now be defined from an earlier const. Docs
  • Rebasing onto a kit's principal. Moving your User onto a kit's subtype of Principal, such as Osysharp.UserAccounts.User, keeps the whole hierarchy in one table, so references to the base still find their rows. Docs
  • Your entity, not the kit's. When your app declares an entity with the same name as one in a kit, the IMPORTED_ENTITY_HAS_NO_POLICY check no longer counts your app's writes to your entity against the kit's.

Kits

  • WorkbenchShell shows the version. With ShowVersion = true, the app and platform versions appear at the end of the status line. Docs
  • MIT, plainly. Each kit's LICENSE is the plain MIT text. Third-party notices are in THIRD-PARTY-NOTICES.md.

MCP

  • whoami says who you are. The built-in whoami tool answers the caller's own account id, as its description says. Docs

Osyrin Cloud

  • Plan limits, per app or per organization. On your organization's Plan page, activity allowances (requests, CPU time, mail, logs) are read for each app, against your busiest app. Holdings (storage, environments, custom domains, collaborators) are read against the organization's total.
  • 0 means none. An allowance of 0 allows none. Only Unlimited means no limit.
  • CPU per second. A plan can set how much CPU time per second an app may use, and how long it may burst above that.
  • Changing plans. Osyrin can move your organization to another plan from its Plan page. Your allowances update straight away.

The release on GitHub, with every asset and its SHA-256

osy 0.17.5 26 October 2026

Language

  • App.Version and App.PlatformVersion. The version of your app that is answering, and the platform release serving it. They are the same in a server function and on a page, and read "dev" when there is no release. Set ShowVersion = true on an AppChrome to show both in the shell. Docs
  • A starting state per row. A goto inside an if in a workflow's Start body now redirects the start. Before, it compiled but the run always entered Initial. Docs
  • More LINQ over lists in hand. xs.GroupBy(x => x).OrderByDescending(g => g.Count()) and lists.OrderBy(l => l.Count()) used to fail at run time and now work. lists.OrderByDescending(l => l.Count()).First().Count() is no longer refused as if it were a database query.
  • Clearer workflow refusals. Wf.For(row) kept in a variable says to read its member in the same expression. An unknown member names the read it lacks.

Runtime

  • Policies read only what they use. A row handed to a policy, as in if (CanWorkOn(item)), is loaded with the columns the policy reads, including through references and through other policies, instead of whole.
  • Hidden pages stay quiet. A page that Outlet(retain: N) keeps alive but hidden no longer re-asks its live server calls on every data change. It asks once, when it is shown again. Docs
  • Page titles set from a component. Navigation.SetTitle and SetTabTitle called inside a component now name the page's route and the browser tab. Before, they did nothing.
  • Compiled loops in the browser. Some loops run as compiled code in the browser. When such a loop read Navigation, Validation or Now, it failed in a real browser with "is not defined", though osy test passed. It now works.
  • Slow calls are named. A slow live server call's log line names the function it asked.

Kits

  • New: Osysharp.ProblemReports. A public wire between an app that sees problems (failed jobs, alerts, errors) and an app that receives them, such as a help desk or a bug tracker. The reporter sends each problem whole, by a key of its own, each time it changes. It retries with a widening wait and never gives up. The receiver keeps one record per key and decides who may report. Docs
  • The workbench keeps its explorer. In WorkbenchShell, when a row of the explorer opens a page outside the nav, the explorer stays beside it, as long as the outlet keeps routes open. A mark with a Badge shows it as a dot, and its words are in the mark's accessible name. Docs

Tooling

  • Compiler bug reports. When the compiler hits a bug of its own (never an error in your code), osy validate, compile, check and test ask once whether to send it to Osyrin. They show exactly what would go first. Nothing is asked when nobody is there to answer. Osyrin keeps only the smallest part of the code that still shows the bug, and deletes that once the fix is released. Docs
  • Test your MCP server. New: Mcp.List and Mcp.Call in a [Test] open a real MCP session against your app's app.McpServer. They return the tools this caller is shown, or what a tool answered. Docs
  • MCP clients follow deploys. A connected MCP client keeps working after a new version of your app goes live. A refusal reaches it as your app's own message, no longer as "An internal error occurred." Docs
  • Assert.Items explains a zero. Rows are counted by identity, so a list of a plain class with no Id counted 0 even when its rows were on the page. The message now says so and names the fix: add public string Id.

The release on GitHub, with every asset and its SHA-256

osy 0.17.4 26 October 2026

Language

  • Reads load what function values use. A function value built in one place and called in another, such as an Http stub a test fixture sets up, now has the columns its body reads loaded with the row. So does a column read through the [Principal] base of a derived entity.

UI kit

  • Facts takes its label. Facts() with no label is now a retired form, like ProgressBar without one. New source is refused and told to write Facts(label). An app already deployed on Osyrin Cloud keeps rebuilding for one more minor version, with a warning on its version. Docs

Tooling

  • Set an account's password. New: osy user set-password and osyrin app user set-password give an existing account of your app a new password. Everything signed in with the old one is signed out, and clients connected through MCP sign-in have to connect again. API keys are not touched. The password is typed at a prompt that does not echo, or comes from --password-file or --password-stdin, never from the command line. Docs
  • Secrets stay off the screen. A refused option used to print your command line back. If that line carried a password, secret, token or key, it is no longer printed. The option is still named, and the hints still follow.
  • New versions find their kits. osy compile --new-version on an app that uses a kit, such as use Osysharp.Charts@1, could be refused as an unknown capability. It now compiles against the kits the deploy carries.
  • osy test names the fixture. When a write in a test is refused inside a function that runs as the app, such as a migration's run or an [OnStart], the message names the fixture to use. It also says that widening the app's security is not the fix.

The release on GitHub, with every asset and its SHA-256

osy 0.17.3 26 October 2026

Language

  • Name a component where you use it. A component call takes label:, which gives that instance its accessible name in place of the one it declares. Two copies of the same component on a page can now be told apart, by people using a screen reader and by your tests.
  • list.Sort((a, b) => …) everywhere. A sort with a comparison now uses it in the browser as well as on the server. Where the comparison could not be honoured, the list was sorted the natural way without a word; that no longer happens.
  • Lambdas inside handlers. onPress: () => Run(() => Bump()) was refused. It now works, exactly like an action you would write out by hand.
  • "Anyone" with no sign-in. In an app without a [Principal], a policy or a VisibleTo that admits everyone, IsAnonymous || IsAuthenticated, now compiles. A VisibleTo policy that never names user needs no principal either.
  • Included children see the caller's changes. A function that reads an .Included collection now sees a child its caller just added or moved there, as the same read without Include already did.
  • static readonly follows the version. A static readonly value whose initializer calls a function in another file is worked out again when a new version of the app goes live, instead of keeping the previous version's value until a restart.
  • Clearer refusals. A function value kept across an await is refused at osy validate, not at run time. A Tool or Endpoint that names an overloaded function says so and lists the overloads. A stream<T> function published as an endpoint or a tool is refused with the long-poll shape that works. The nullable-read lint follows the code's flow, so "if it is missing, make one" counts as present.

Runtime

  • Pages load fully after an update. Right after Osyrin Cloud moved to a new version, the first visit to a page could fail to load some of its parts. That no longer happens.
  • Updates reach open pages at once. When the platform moves to a new version, open live connections are closed straight away, so pages reconnect to the new version instead of holding the old one up.
  • A fresh start is not charged to your app. Loading your app onto a server is the platform's own warm-up, so its first visitors are not slowed for using up the app's CPU allowance.
  • Pages wait for their data. A component that reads live server data shows its body once the data has arrived, skeleton or not, instead of briefly rendering without it. A layout whose data arrives late still shows its page.
  • Moving entities under a shared base keeps their data. When several entities are rebased onto one base type, every row moves into the base's table with its id kept and every reference re-pointed, in the same transaction as the deploy. This works for local apps and for apps on Osyrin Cloud, including a base that already holds rows. Docs
  • Quieter logs. A refusal your app declares (not found, invalid, conflict) is logged without a stack trace, and a request the visitor walked away from ends quietly instead of as an error. A page that fails to render in a visitor's browser is now logged on the server, naming the page.

Osyrin Cloud

  • Clearer deploy failures. A deploy refused by its region fails at once with the reason. A migration file written for a refused deploy now passes that deploy's own check when you apply it.
  • osyrin org apps --org. Lists the apps of the organization you name, and says which organization it is.
  • osyrin app query now reaches an app wherever Osyrin Cloud runs it.

Tooling

  • Passwords off the command line. --password-file and --password-stdin on osy user add, osy import, osyrin app user add, app login, app import and app run, so a password never shows in the process list. A password file others can read is refused, naming the chmod that fixes it. Docs
  • Imports say what they refused. osy import and osyrin app import count refused rows as refused, never as unchanged, and show the first reason. Re-importing a row whose key has an empty part no longer makes a duplicate. Docs
  • user add --set lists what you can set, and says where the sign-in and password come from.
  • Migrations propose the right move. --generate-migration proposes a rebase, not a drop, when an entity gains a base that carries its members. Export and --generate-migration no longer stop on a page with [Authorize].
  • Local compiles need no backup store. A compile that would change data on your own machine no longer asks for a backup it has nowhere to keep.
  • Help that helps. An option given to a command group names the commands that take it. A refusal's "see osy docs …" pointer always names a topic.

The release on GitHub, with every asset and its SHA-256

osy 0.17.0 25 October 2026

Breaking changes

  • [OwnUnitOfWork] is now [CommitsAlone]. Same meaning, clearer name, no alias. Fix: rename the attribute, or run osy migrate code.
  • ProgressBar takes a label. ProgressBar(label, percent, tone): a progress bar always says what it measures. Fix: pass the label first, or run osy migrate code.
  • Zone is now TimeZoneInfo. The old spellings (Zone, Zone.Of, Zone.Declared, .InZone, .OffsetAt, .IsDst) are refused, and the message names the C# spelling to use. Fix: run osy migrate code, or use the one the message names, for example TimeZoneInfo.ConvertTimeFromUtc(...) where you wrote .InZone(...). Docs

Language

  • Time zones, the C# way. A time zone is a TimeZoneInfo, with the members you know: Id, DisplayName, BaseUtcOffset, GetUtcOffset, IsDaylightSavingTime, TimeZoneInfo.Utc, ConvertTimeFromUtc, ConvertTimeToUtc and FindSystemTimeZoneById. A zone id written as a literal is still checked when you compile, against the zones your app declares. Docs
  • Local time that follows the app. DateTime.Now, Today and ToLocalTime follow the app's app.DefaultTimeZone, on the server, in the browser and in queries. Mark a field [PreferredTimeZone] and local time follows each signed-in viewer's own zone. Docs
  • DateTime.Parse reads into local time. Text with an offset, such as 2026-07-01T12:00:00+01:00, comes back in the app's local time, on the server and in the browser alike. In an app with no zone, the answer no longer depends on the machine the server runs on. Docs
  • Translations survive a placeholder rename. A translation keyed before a placeholder was renamed is still served, and still counts as translated in osy translate.
  • More of C# just works. foreach (Thing t in things) with a typed variable in a render block; new HashSet<T>(items) and new Dictionary<K,V>(map) copy constructors; async Task<T> F<T>() and Boxed<T> F<T>() pass osy validate; s.Like() works as an instance method; Type == Type holds when two values name the same type.
  • Aggregates match C#. xs.Max() over an in-memory list no longer answers 0 for an empty list. It throws, as in C#.
  • Better refusals. An out argument to a function that declares none, and an or pattern that binds a variable, are refused with the C# error and the line they sit on. A row column named Thread is the column, not System.Threading.Thread. Deeply nested code now compiles instead of failing. [Authorize(IsA || IsB)] on a page tells you to declare a policy for the pair, and a [Setting] of the wrong type names TimeZoneInfo as the type it takes.
  • Fewer wrong answers. xs.Any(p) over an in-memory list is the query it should be. Numbers passed into a decimal parameter are widened to fit. A [Role] enum used as a principal field says which two declarations you need.

Runtime

  • Reads load only what they use. A read now fetches only the columns the code actually uses, including across references, Include, workflows and type tests, with no extra queries. A row that was read in part is never treated as whole.
  • Organization time zone. Each organization has one Time zone setting. An app that declares no zone of its own uses it.
  • Pages left open keep working. A signed-out visitor who leaves a page open for a long time can carry on where they were. The page renews its access by itself instead of failing on the next click.
  • Sorting a paged table. A table that sorts on the server could keep showing the first order it rendered, whichever column the viewer chose. It now sorts by the column chosen.
  • Small apps stay small. A new app gets only the platform tables it uses. The tables for row grants, events and outbound calls appear when the app first needs them, and keep their rows if it stops.
  • Busy is not broken. An app at its capacity answers "try again shortly" (503) instead of an error (500).

Security

  • Deny rules through an empty reference. A rule such as deny delete where Customer.Name == "Protected" refused deleting one order with no customer, but a delete of many rows at once let it through. Both now refuse it. To exempt rows with no customer, write Customer != null && ….
  • Organization roles take effect at once. Access to an organization's apps follows each person's current role in the organization. Someone made an owner is recognised straight away, and someone who stops being one loses that access straight away.

UI kit

  • Plainer kit listing. osy kit points at the other kits, and a search no longer answers on one stray word.

Tooling

  • osy migrate code. New. It rewrites the retired forms in your project, such as the three breaking changes above, then validates again. --dry-run shows what it would change, and anything it cannot rewrite is listed for you.
  • Retiring a form gently. When a spelling is retired, new source that writes it is refused and told what replaced it. An app already deployed on Osyrin Cloud keeps rebuilding on new releases for one more minor version, with a warning on its version that names the form and the release that will refuse it. Kits can retire a former signature of their own controls the same way.
  • osy test catches refused reads. A page whose data read was refused used to pass its test while showing nothing. Now the test fails, unless it asserts the refusal, and each fault is reported once.
  • Advice that keeps data closed. When a public page reads the table of role grants, the advice now says to ask a policy, never to open the table to visitors.
  • No statistics from CI. osy running in a CI job no longer sends usage statistics. OSY_TELEMETRY=off still turns them off everywhere. Docs

The release on GitHub, with every asset and its SHA-256