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.
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.
osy 0.17.6 27 October 2026
Language
asinside 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 aUser.- Policies in a component's body. A policy named in
on mount, an action or a method of a component, as inif (IsPlatformAdmin) { … }, used to compile but fail when the page loaded. It now works. Outlet(retain:)takes a constant.retain:accepts a literal, arithmetic, or aconstof the layout, such asOutlet(retain: OpenTabs * 2). A value known only at run time is refused, and the message says why. Aconstmay now be defined from an earlierconst. Docs- Rebasing onto a kit's principal. Moving your
Useronto a kit's subtype ofPrincipal, such asOsysharp.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_POLICYcheck no longer counts your app's writes to your entity against the kit's.
Kits
WorkbenchShellshows the version. WithShowVersion = true, the app and platform versions appear at the end of the status line. Docs- MIT, plainly. Each kit's
LICENSEis the plain MIT text. Third-party notices are inTHIRD-PARTY-NOTICES.md.
MCP
whoamisays who you are. The built-inwhoamitool 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.
osy 0.17.5 26 October 2026
Language
App.VersionandApp.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. SetShowVersion = trueon anAppChrometo show both in the shell. Docs- A starting state per row. A
gotoinside anifin a workflow'sStartbody now redirects the start. Before, it compiled but the run always enteredInitial. Docs - More LINQ over lists in hand.
xs.GroupBy(x => x).OrderByDescending(g => g.Count())andlists.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.SetTitleandSetTabTitlecalled 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,ValidationorNow, it failed in a real browser with "is not defined", thoughosy testpassed. 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 aBadgeshows 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,checkandtestask 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.ListandMcp.Callin a[Test]open a real MCP session against your app'sapp.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.Itemsexplains a zero. Rows are counted by identity, so a list of a plain class with noIdcounted 0 even when its rows were on the page. The message now says so and names the fix: addpublic string Id.
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
Httpstub 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
Factstakes its label.Facts()with no label is now a retired form, likeProgressBarwithout one. New source is refused and told to writeFacts(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-passwordandosyrin app user set-passwordgive 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-fileor--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-versionon an app that uses a kit, such asuse Osysharp.Charts@1, could be refused as an unknown capability. It now compiles against the kits the deploy carries. osy testnames the fixture. When a write in a test is refused inside a function that runs as the app, such as a migration'srunor an[OnStart], the message names the fixture to use. It also says that widening the app's security is not the fix.
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 aVisibleTothat admits everyone,IsAnonymous || IsAuthenticated, now compiles. AVisibleTopolicy that never namesuserneeds 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 withoutIncludealready did. static readonlyfollows the version. Astatic readonlyvalue 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
awaitis refused atosy validate, not at run time. AToolorEndpointthat names an overloaded function says so and lists the overloads. Astream<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 querynow reaches an app wherever Osyrin Cloud runs it.
Tooling
- Passwords off the command line.
--password-fileand--password-stdinonosy user add,osy import,osyrin app user add,app login,app importandapp run, so a password never shows in the process list. A password file others can read is refused, naming thechmodthat fixes it. Docs - Imports say what they refused.
osy importandosyrin app importcount 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 --setlists what you can set, and says where the sign-in and password come from.- Migrations propose the right move.
--generate-migrationproposes a rebase, not a drop, when an entity gains a base that carries its members. Export and--generate-migrationno 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.
osy 0.17.0 25 October 2026
Breaking changes
[OwnUnitOfWork]is now[CommitsAlone]. Same meaning, clearer name, no alias. Fix: rename the attribute, or runosy migrate code.ProgressBartakes a label.ProgressBar(label, percent, tone): a progress bar always says what it measures. Fix: pass the label first, or runosy migrate code.Zoneis nowTimeZoneInfo. The old spellings (Zone,Zone.Of,Zone.Declared,.InZone,.OffsetAt,.IsDst) are refused, and the message names the C# spelling to use. Fix: runosy migrate code, or use the one the message names, for exampleTimeZoneInfo.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,ConvertTimeToUtcandFindSystemTimeZoneById. 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,TodayandToLocalTimefollow the app'sapp.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.Parsereads into local time. Text with an offset, such as2026-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)andnew Dictionary<K,V>(map)copy constructors;async Task<T> F<T>()andBoxed<T> F<T>()passosy validate;s.Like()works as an instance method;Type == Typeholds 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
outargument to a function that declares none, and anorpattern that binds a variable, are refused with the C# error and the line they sit on. A row column namedThreadis the column, notSystem.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 namesTimeZoneInfoas 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, writeCustomer != 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 kitpoints 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-runshows 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 testcatches 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.
osyrunning in a CI job no longer sends usage statistics.OSY_TELEMETRY=offstill turns them off everywhere. Docs