A programming language · beta
The whole application is one program.
Osy# is one language with one application model. Data, security, functions, screens, workflows, agents and tests are declared in the same program. The compiler understands all of it, and the runtime executes it as one system.
An order has data, a rule about who may change it, an action that changes it, and a page that shows it. Here those are one program. Nothing in it says where any part runs, and nothing needs to.
model/board.osy · compiles as printedentity Order { string Reference; bool Shipped; security { allow read, update when IsAuthenticated; } } [Route("/")] component Board() { live var open = Order.Where(o => !o.Shipped); action Ship(Order order) { order.Shipped = true; } render { foreach (var o in open) { Row { Text(o.Reference); Button("Ship", onPress: () => Ship(o)); } } } }
01 · Access
Access belongs to the data.
Who may see or change a record should not depend on which page, function or other part of the application happens to ask. So the rule is declared once, on the entity, and the compiler puts it inside every query and every write over that entity.
Here is an expense report from a larger application, with the policies its rule uses. A policy is a
named condition on the caller, written once, and it can take an argument: finance in this
organisation. The rule says who may see a report (the employee who filed it, their manager, finance and
the controller of its organisation) and that its owner may change it only while it is a draft or sent back.
Each line is either a condition on the row (where) or on the caller (when),
and the lines add up.
Nothing that reads or writes a report has to reproduce this rule, and nothing can bypass it. The rule is compiled into every query and every write over the entity, wherever in the program it comes from. A query may ask for less than the rule allows, by adding conditions of its own; it cannot ask for more. The next movement shows what those reads and writes look like.
Declare access on the data. Every read and write obeys it, wherever it originates.
apps/spendflow · identity.osy and expenses.osy · abridgedpolicy IsFinance(Organization o) => RoleGrant.Any(g => g.User == user && g.Role == Role.Finance && g.Organization == o); policy IsController(Organization o) => RoleGrant.Any(g => g.User == user && g.Role == Role.Controller && g.Organization == o); entity ExpenseReport { [Required("An organisation is required.")] Organization Organization; [Required("An employee is required.")] Employee Employee; [Required("Give the report a name."), MaxLength(140, "Keep the title under 140 characters.")] string Title; ReportStatus Status = ReportStatus.Draft; DateTime? SubmittedAt; … security { allow read where Employee.User == user; allow create where Employee.User == user; allow update where Employee.User == user && (Status == ReportStatus.Draft || Status == ReportStatus.Rejected); … allow delete where Employee.User == user && Status == ReportStatus.Draft; allow read where Employee.Manager.User == user; allow read where IsFinance(Organization) || IsController(Organization) || …; } }
The entity from SpendFlow, an expense app built on kits, with two of the policies its rule names. Abridged
where marked: more fields, the per-column rules for the moments a report is submitted, and two more roles.
RoleGrant is an entity the app declares too: a row per person, role and organisation. The
Required and MaxLength messages are what a person sees when the rule is broken.
02 · Data
You write LINQ. It becomes one statement.
An application reads its data in many shapes: a filtered list, one row, a total, a page of results. It also changes sets of data at once: close every stale order, purge what is finished. In Osy#, they are all expressions over the same entity model.
Each one is C# LINQ over the entity, and the compiler turns the chain into one SQL statement. The condition runs in the database, never over rows fetched first, and a total is computed there too. A verb that cannot become SQL is refused at compile time rather than run quietly over the whole table.
A live read is the same query kept current: re-evaluated when the data it read changes,
so the page that reads it follows. And a write uses the same vocabulary. Update ends a chain
with one set-based statement, and answers how many rows it changed.
None of these chains filters by who is asking, and none needs to. The platform applies the entity's rule to every one of them: a read returns the rows the caller may see, and a bulk write changes exactly the rows the chain would have returned, narrowed further by the entity's update rule. A row the caller may not see is simply not in the set.
One vocabulary for every read and every write, with the access rule inside each statement.
from the query reference, where each is compiled and run// a filtered, ordered, bounded list — one SELECT List<Order> BigOpenOrders(decimal floor) { return Order.Where(o => o.Total > floor && !o.Cancelled) .OrderByDescending(o => o.Total) .Take(20) .ToList(); } // one number, computed by the database decimal Revenue() { return Order.Where(o => !o.Cancelled).Sum(o => o.Total); } // a live read — the same query, kept current as data changes live var open = Order.Where(o => !o.Shipped); // a bulk write — one statement; it answers how many rows int CloseStale() { return Order.Where(o => o.Status == "Stale") .Update(o => { o.Status = "Closed"; }); }
Four chains from the query reference, where each is compiled and run against a real
database by the gate that checks this site; the live read is the hero's. The rule on Order is
applied to all four by the platform: the caller's rows, and only those. Grouping, projections, paging,
Include, Delete and Insert are in the query
reference.
03 · The compiler
The compiler checks the parts against each other.
Add a second page and make it public, so visitors can see the open orders without
signing in. On its own, that page is fine. On its own, the rule on Order is fine. Together
they contradict what the application is trying to do: the page will render, and its query will return
nothing, because Order grants anonymous visitors no read.
The compiler warns before the application runs. It can, because the page, its query and the entity's rule are parts of one program it holds at once. Neither declaration is wrong by itself; the mistake exists between them, and the warning names both sides and the fix.
A mistake between two parts of the application is found before the application runs.
model/open.osy[AllowAnonymous] [Route("/open")] component OpenOrders() { live var open = Order.Where(o => !o.Shipped); render { foreach (var o in open) { Text(o.Reference); } } }
$ osy lint · abridgedSHOULD • OpenOrders → Order — 'OpenOrders' is `[AllowAnonymous]` and reads 'Order', but 'Order' grants no anonymous read, so for a visitor with no credential the page renders and its query returns ZERO ROWS — no error, just an empty page. `[AllowAnonymous]` opens the element surface; the row read is gated separately at the entity by its own `security { }` block, and the two do not inherit. model/open.osy:1:2 security-anon-page-reads-ungranted-entity · fix: add `allow read when IsAnonymous;` to 'Order's `security { }` …
Above, the public page, added to the application from the hero. Below, what
osy lint says about the whole application: the linter is part of the compiler's toolchain, judges
the whole program as the compiler sees it, and reports each finding by name, with where it is and what to write. This one is a
warning, because the page is legal; it is just not what anyone meant.
04 · Lifecycle
A process that waits three days for a person is still just source.
A support ticket is opened, answered, sometimes parked on the customer, resolved, occasionally reopened. Different people act at different stages, there are promises about how quickly, and the desk waits hours or days for someone. After a deploy or a restart, every open ticket must still mean the same thing.
In Osy# that lifecycle is a workflow on the ticket's own status column. A state says which events it accepts and who may raise them; an event moves the ticket to the next state. Here a support agent's first reply moves an open ticket to Working; asking the customer parks it; the customer's reply brings it back. Between events the run waits, for as long as it takes, and it outlives the deploy that started it.
A real desk needs more: a first-reply and a resolution promise, a clock that counts in business hours
and pauses while the customer is being waited on, and escalation before a promise breaks. In Harbour, the helpdesk built from kits, those are not written per application: they
come with the inbox kit, Osysharp.WorkQueues, and the ticket's own workflow stays this
small. The workflows chapter reads one top to bottom.
Your database is transactional. The world is not. A rollback undoes rows; it does not unsend an email or uncharge a card. So every call that leaves the platform is made a durable step by the compiler, and after a crash or a restart a call already recorded is not made again. You write the ordinary call; the compiler finds it.
Waiting, resuming and moving through the lifecycle are semantics of the program.
a support ticket's lifecycle · compiles as printedworkflow SupportTicket { Tracks = Ticket.Status; Autostart = true; Initial = Open; event Respond(string reply); event AskCustomer(); event CustomerReplied(); state Open { subscribe Respond(string reply) as FirstReply { Candidates = u => u.Team == Team.Support; } on FirstReply(string reply) { goto Working; } } state Working { subscribe AskCustomer(); on AskCustomer { goto AwaitingCustomer; } } state AwaitingCustomer { subscribe CustomerReplied(); on CustomerReplied { goto Working; } } terminal success Closed { } }
A support ticket's workflow, reduced to its shape; it compiles as printed (re-validated
2026-10-16). subscribe declares that a state waits for an event; Candidates says
who it is offered to. Harbour, the helpdesk built
from kits, runs the full version.
05 · Proof, per person
Test the application as the people the rules are about.
The order-lifecycle sample says only a manager may cancel an order, and only fulfilment staff may ship one. Does the application behave that way for the actual people? A test signs in as them, drives the real page, and asserts what each one can see and do.
The fixture creates Fael and Mona as rows and gives Fael the Fulfillment role and Mona the Manager
role; principal names them for the test. Ui.SignInAs opens the real page as that
person, on a database that starts for the test, and the two asserts check what the rule put on it.
Nothing in the test stands in for the application.
The rules can be proven by running the application as the people they govern.
demo/wf-order-saga/tests/board.test.osy · abridgedprincipal Fael => Staff.Single(s => s.Name == "Fael"); principal Mona => Staff.Single(s => s.Name == "Mona"); [TestFixture] void OneOrderAndTwoTiers() { var fael = new Staff { Name = "Fael" }; var mona = new Staff { Name = "Mona" }; new RoleGrant { Grantee = fael, Level = AppRole.Fulfillment }; new RoleGrant { Grantee = mona, Level = AppRole.Manager }; … } [Test(OneOrderAndTwoTiers)] void only_a_manager_may_cancel() { // name shortened Ui.SignInAs(Fael); Ui.Visit("/"); … Assert.Hidden("Cancel order"); Ui.SignInAs(Mona); Ui.Visit("/"); … Assert.Visible("Cancel order"); }
A test file from the order-lifecycle sample, abridged where marked (the email each row also
carries, and the test's longer name). The fixture writes the
rows every test in it starts from; a test runs against a database that starts for it, with the real
application on top. Assert.Visible and Assert.Hidden look at the real page.
06 · Kits
Kits are finished pieces of an application.
Data, security, workflows, screens and tests, done once. An application inherits them with one line, and writes only what makes it different.
Osysharp.Mail is not a mail API library. Osysharp.UserAccounts is not
authentication plumbing. Osysharp.Conversations is not a collection of messaging utilities.
Each is a finished piece of an application, written in Osy# like the application itself: its entities
carry their access rules, its workflows wait, its screens draw, and its tests run with yours.
Beside this is a whole helpdesk, Harbour: mail, text and chat in one thread per customer, queues and service levels, a desk worked from the keyboard and a portal for customers. Its dependency list is 33 kits. What the application adds is what a ticket is: a number, a status, a type, tags, a merge. The same holds for the online shop, whose catalogue, accounts, payments, returns and back office are kits under eleven different looks.
Where a kit leaves a choice to the application, it says so as a contract, and the compiler checks the application against it the way it checks a class against an interface.
Agents compose the kits and write what makes your application different.
Apps/harbour/app.osy · the kits, comments removedapp Helpdesk { use Osysharp.Identity@0; use Osysharp.UserAccounts@1; use Osysharp.CustomFields@1; use Osysharp.Organizations@0; use Osysharp.Organizations.ServicePlans@0; use Osysharp.Invitations@2; use Osysharp.UserManagement@0; use Osysharp.Mail@0; use Osysharp.Mail.Receiving@0; use Osysharp.Mail.Gmail@0 { egress "oauth2.googleapis.com"; egress "gmail.googleapis.com"; } use Osysharp.EmailTemplates@0; use Osysharp.Conversations@0; use Osysharp.Presence@0; use Osysharp.WorkQueues@1; use Osysharp.LoadingOrbs@0; use Osysharp.ReplyComposer@0; use Osysharp.Automations@0; use Osysharp.Notifications@1; use Osysharp.SatisfactionSurveys@0; use Osysharp.KnowledgeBase@1; use Osysharp.MarkdownEditor@1; use Osysharp.Seo@0; use Osysharp.Webhooks@0 { egress "*"; } use Osysharp.ProblemReports@0; use Osysharp.Privacy@0; use Osysharp.Reports@1; use Osysharp.Charts@1; use Osysharp.Csv@0; use Osysharp.TimeZone@0; use Osysharp.Llm@2; use Osysharp.Models.Anthropic@1 { egress "api.anthropic.com"; } use Osysharp.MachineTranslation@0; use Osysharp.Agents@1; model "model/**/*.osy"; }
07 · Agents
An agent is part of the program, and it obeys the same rules.
An agent is given real work: estimate a repair, vet a supplier, take a step of a process that runs for days. The question is never whether the model is clever enough. It is what the agent may touch, who decides, and what it costs.
In Osy# an agent is a class. Its skill is a typed job whose tools are the application's own functions,
and it runs as an account of its own that holds the roles the class names. Working for a person, it reads
and changes only what both of them may. The access rules from 01 are not repeated for it; they
already apply. And the agents themselves are a kit, Osysharp.Agents, with the inbox,
processes and requests to people beside it as kits of their own.
What it may do alone, what it must propose to a person first and what it never does is declared on the class. A proposal lands in that person's inbox as the exact call, and a yes runs it as them. Every piece of work has a hard spending limit, a receipt, and a record of each step that a person who was not there can read.
Agents are a construct of the language, so the compiler, the security model and the tests already cover them.
from reference/agent/agents · abridged · its tests run in CIentity Repair { [Required, MaxLength(200)] string Fault; decimal Price; security { allow read, update when IsMechanic || IsManager || IsHelper; deny update Price when IsHelper; } } public class EstimateRepair : Skill<Repair, Estimate> { public override string Description => "Estimate one repair."; public override List<ActionInfo> Tools => [Actions.Of(PartsFor), Actions.Of(NoteRepair), Actions.Of(SetPrice)]; public override string? Check(Estimate e) => e.Hours <= 0 ? "An estimate takes some time." : null; } [Actor(Roles = [ShopRole.Helper])] public class Estimator : Agent { public static Person Principal => new Person { Email = "[email protected]" }; public override string Purpose => "You estimate bicycle repairs for a small workshop."; public override List<SkillBase> Skills => [new EstimateRepair()]; public override Permits May => Permits.ChangesAlone(); } // give it work — the answer is an Estimate, checked, or how the work ended var outcome = await Agents.Of<Estimator>().Do(new EstimateRepair(), repair);
One agent from the agents reference, abridged. The mechanic Mia may set a price; the
agent holds Helper, and a helper may not, so when it works for Mia its SetPrice
tool is refused and the model is told. The reference page runs that as a test, with the model scripted.
08 · The reader that cannot ask
More of your code is written by something that cannot ask a colleague.
It has the source and the toolchain, and no way to ask what was meant or how something is spelled. Everything this page has shown is explicit in the program, so the toolchain can answer.
osy docs answers how a thing is written, from the reference, by name, alias or plain
question. osy model answers what the application is, as the compiler sees it. osy explain
answers who may read and write what, from the declarations. osy lint and osy check
answer whether a change is right, and a finding says what to write instead. osy inspect
answers what actually ran, step by step. Every one of them answers --json.
Beside this: a question nobody on the team may have asked before, how to read a Snowflake warehouse,
put to osy docs. The answer is the package, what to declare, and a table as an entity
with the rule from 01 on it. Bring the agent you already use; osy init --agent claude writes
its instructions today.
What the application means, and how it is written, does not have to be inferred when the toolchain can answer.
$ osy docs snowflake · abridgedmatched on alias: snowflake → config-snowflake The Snowflake kit — warehouse tables as entities, and exports back (config-snowflake) An optional package that makes Snowflake tables ordinary Osy# entities: your app reads them with LINQ, and each query runs in Snowflake as one statement already carrying the reading person's access rule. The other direction sends your own entities into Snowflake tables as they change. sections — ask for any number with osy docs config-snowflake#<name>: #summary #signature #install #credentials #entities #queries #writes #exports #limits … ── app.osy — the dependency, and the grant it cannot work without {#install} app Revenue { model "**/*.osy"; use Osysharp.Warehouses.Snowflake { egress "*.snowflakecomputing.com"; secret "SnowflakePrivateKey"; } } ── one table, one entity — the access rule travels with every query {#entities} [DataSource(DataSource.Warehouse), ExternalName("PUBLIC.ORDERS")] entity Order { [Key, ExternalName("ORDER_ID")] string Number; [ExternalName("REGION")] string Region; [ExternalName("TOTAL")] decimal Total; security { allow read where Region == user.Region; } } …
The reference page for the Snowflake package, as the toolchain prints it: what to declare, the grant it needs, and a warehouse table as an entity with the same kind of rule as in 01, applied to every query the same way. Every compiled example on the page is checked by the same gate that compiles this site's reference, which is why the answer can be trusted without a person to check it.
Get started
From an empty folder to a running application.
Give your agent a project it understands, and watch it build, check and run an application on your machine. The toolchain goes to the people in the beta first.