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

Written by agents

A language that answers questions, for a reader that cannot ask a colleague.

More of your code is being written by something that has no tribal knowledge, no teammate to interrupt, and no memory of why this field is nullable. What it has is the source, the compiler, and whatever the compiler is willing to say. Osy# was built to be asked — and to make a wrong answer land somewhere it cannot do harm.

osy model

What the app IS — resolved, not inferred from the source.

osy explain

Who can read what, computed from the declarations.

osy inspect

What actually ran, end to end, step by step.

osy check

One verdict, and a refusal that carries its fix.

01

The reader changed. Most languages have not noticed.

Every language quietly assumes its reader can do things a machine cannot.

Ask a colleague which of these forty tables the invoice one really is. Notice that the auth check three files away is why this query returns nothing. Remember that Amount went nullable because of a migration in 2023. None of that is written down. All of it is load-bearing.

An agent has none of it — and unlike a new hire, it will not come back and ask. It will produce something plausible instead. So the useful question is not whether the model is good enough. It is whether the language will tell it the truth when it asks, and what happens when it does not.

Everything below is the ordinary toolchain. Not a plugin, not an MCP server, not a prompt.

02osy model

Ask what the app is.

One command, and the whole resolved model — not the files, the model the compiler built out of them. No database, no server.

$ osy model --json
{ "summary": …, "capabilities": …, "entities": [ … ], "provided": …,
  "providedEnums": …, "enums": …, "functions": …, "workflows": …,
  "clients": …, "components": …, "themes": …, "topics": …, "apis": … }

// one entity out of that, verbatim
{ "name": "Invoice", "kind": "entity",
  "fields": [
    { "name": "Amount", "type": "decimal", "required": true },
    { "name": "Owner",  "type": "User",
      "relation": { "target": "User", "kind": "reference" } } ],
  "security": { "posture": "deny",
    "rules": [ { "effect": "allow", "operations": ["read"],
                 "hasRowFilter": true } ] },
  "platformFields": ["Id", "CreatedAt", "ModifiedAt", …] }

Every field with its real type. Every relation wired to its target. Each entity's security posture as the compiler computed it. The alternative is what an agent does everywhere else — grep for a class name, read three files, and infer. Inference is where the wrong answers come from.

03osy explain

Ask who may read what.

Generated from the same declarations the runtime enforces — so it cannot drift from the behaviour, which is the failure mode of every security document ever written.

entity Invoice {
  [Required, MaxLength(80)] string Reference;
  [Required] decimal Amount;
  User Owner;
  security {
    allow read where Owner == user;
    allow create, update where Owner == user;
  }
}

[Principal] entity User {
  [Unique, MaxLength(200)] string Email;
  [MaxLength(200)] string PasswordHash;
  security {
    allow read when IsAuthenticated;
    allow read, create when IsAuthenticator;
    deny read PasswordHash when !IsAuthenticator;
  }
}

// RoleGrant declares no security block at all.
entity RoleGrant { User Grantee; [Required] AppRole Level; }
$ osy explain
# Security report — who can do what

> Posture: deny-all. An entity with no `security { }` block
  grants no access to anyone.

## `RoleGrant` — deny-all
> No `security { }` block, and deny-all is in force — this
  entity grants NO access to anyone.
- Read — No one (denied by default).

## `Invoice` — default-deny
- Read — Only their own rows (where Owner is the caller).
- Delete — No one (denied by default).

## `User` — default-deny
- Read — Any signed-in user; or the caller holds the
  Authenticator role — `PasswordHash` carries a rule of its own.
Fields
- The PasswordHash field is withheld from a reader unless the
  caller holds the Authenticator role.

04osy validate

A refusal that carries its own fix.

Error messages are the largest surface an agent reads. They were written as such — because a diagnostic that names a line and stops is a search, and a search is a wrong turn waiting to happen.

// Invoice.Amount is a decimal, and Max over NO rows has no value
// to answer with — so this returns null through a `decimal`.
decimal biggest() {
  return Invoice.Max(i => i.Amount);
}
$ osy validate
model/bad.osy:1:39  ERROR  RESOLVE_ERROR
this function returns 'decimal', which cannot hold null — but this
is the result of a `Max(…)` aggregate, which answers null over NO
ROWS — `Min`/`Max`/`Average` have no zero to fall back on, where
`Sum` and `Count` answer 0, and nothing here says what to return
when the value is absent. Answer for absence at the `return` —
`?? 0m` if that is what absence means here — or declare the
function 'decimal?' and let it be absent.
  → for more, RUN: osy docs query-aggregates

Three things there that an ordinary error does not have: why the rule exists, both remedies rather than the first one, and the contrast that makes it stick — Sum and Count behave differently, and now you know that too.

What a guess cannot turn into

A Max(…) returned through a non-nullable decimal
refused
It answers null over no rows. Refused in every position where that null could escape — the local, the column, the return.
new List<string> { "a", "b" }
compiles
Ordinary C#, and it compiles. The language is not a subset whose edges you have to learn first.
A stdlib call with no SQL form, inside a query
refused
It would work in memory and fail against the database. osy check refuses it before you run.
int x; read on a path that never assigned it
refused
Definite assignment, as in C#. The value that would have been null cannot be observed.

And the whole of it survives into --json, message and doc topic included — so the loop does not have to scrape a terminal.

$ osy validate --json
{ "success": false, "files": 3,
  "errors": [ { "file": "model/bad.osy", "line": 1, "column": 39,
                "code": "RESOLVE_ERROR", "message": "…",
                "docTopic": "query-aggregates" } ],
  "warnings": [], "symbols": { "entities": ["Invoice", …] } }

05osy inspect

Ask what actually ran.

A compiler can only tell you about the program. The other half of debugging is the run — and that is the half an agent normally cannot see at all, so it re-reads the code and guesses again.

// The test, and the function it calls. Three parameters, two locals —
// which is what makes the trace below worth reading.
[Test] void a_rate_is_applied() {
  Assert.Equal(12.35m, applyRate(49.4m, 0.25m, "quarterly"));
}

decimal applyRate(decimal amount, decimal rate, string note) {
  var scaled  = amount * rate;
  var rounded = Math.Round(scaled, 2);
  return rounded;
}
$ osy trace start --full
$ osy test
$ osy inspect
╭───────────┬───────────────────┬───────────┬───────┬──────────╮
│ trace     │ function          │ outcome   │ steps │ started  │
├───────────┼───────────────────┼───────────┼───────┼──────────┤
│ e9b6cab1… │ a_rate_is_applied │ Completed │     7 │ 15:32:00 │
╰───────────┴───────────────────┴───────────┴───────┴──────────╯

$ osy inspect e9b6cab1
    0  server stmt    a_rate_is_applied
    1  server eval    applyRate
       amount = 49.4
       rate = 0.25
       note = "quarterly"
    3  server stmt    applyRate
       scaled = 12.350
    4  server return  applyRate
       scaled = 12.350
       rounded = 12.35
    5  server stmt    a_rate_is_applied

$ osy inspect e9b6cab1 --step 3
// the STACK at that moment, innermost first
  server applyRate
    scaled = 12.350
  server a_rate_is_appliedapplyRate
    amount = 49.4
    rate = 0.25
    note = "quarterly"
  server a_rate_is_applied
    (no locals in scope)

The values, not just the shape. Every step names the function it ran in and carries the locals in scope; --step unrolls the whole stack at that moment, frame by frame, with the arguments each call was made with — a_rate_is_applied → applyRate reads as what it is. That is the difference between “the function was entered” and “the function was entered with 0.25”, which is usually the entire question.

Server and client under one correlation id. And a fault is captured whether recording was on or not — the thing you most need to see is the thing you were least likely to have prepared for. --fault jumps straight to the throw site.

Four verbs, and the app has stopped being a guessing game. Now make being wrong cheap.

06

And the guess that goes wrong reaches nothing.

Everything above makes an agent right more often. This is the part that decides whether letting one near your data was a good idea.

In most stacks, authorization is a thing you remember to write. A missing check is invisible: the code reads fine, the tests pass, the endpoint returns everyone's rows. That is a bad failure mode when a person writes it. It is a much worse one when the author is a machine producing plausible code at speed — because plausible is exactly what a missing check looks like.

Here, access is declared on the entity and the default is deny. An entity with no security { } block grants nobody anything: the empty state is the safe one, not the open one. There is no check to forget, because there is no check to write — the filter is applied to every read the language can express, by the engine, underneath.

The shape of a mistake

An agent adds an entity and writes no security block
compiles
It compiles — and grants nobody anything. The mistake costs a refusal, not a leak.
An agent writes a query and forgets to filter by owner
compiles
It compiles, and returns the caller's rows anyway. The row filter was never in the query.
An agent writes code to get around the filter
refused
There is no expressible way to. Security is not a library the program calls; it is applied beneath every read.
An agent reads a password hash to "verify" something
refused
The field carries its own deny. A masked column is not in the SELECT at all.
Discoverable

So it is right more often

It can ask what the app is, who may read what, and what actually happened — and get an answer that is true of THIS app, not of the framework in general.

Default-deny

So being wrong is bounded

The blast radius of a bad guess is set by a declaration, not by whether the guesser remembered to check. Nothing it can write reaches data the declaration did not open.

Either one alone gives you a fast way to make a mess, or a safe way to be useless.

07osy check

The loop, in full.

Shorter than this page makes it look.

$ osy init --agent claude   // scaffolds CLAUDE.md + settings
$ osy model --json         // what am I working on
$ osy docs <anything>      // how is this spelled
   … write the code …
$ osy check                // validate + lint + test — ONE verdict
$ osy inspect --fault      // when it ran, and was wrong

osy check is one command and one answer, which is the shape a loop wants — not three whose output has to be reconciled. Nearly all of it answers --json. And osy docs exists so the vocabulary comes from the platform rather than from reading the compiler's source, which is what an agent does when nothing better is on offer.

Where to go next

Security

Default-deny in full: row filters, field masks, and the two things it deliberately does not do.

Agents

The other direction — agents INSIDE your app: declaring one, streaming its answer, parking it on a person.

One program

Where the compiler put each line, and why you did not choose.