Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne program

Reference / Security

rows that are part of another row

partial entity Schedule { security { … } } // its rules, times and exclusions follow

Some platform shapes decompose into several tables — a schedule owns its rules, and a rule owns its times, weekdays and month-days. Those child rows cannot exist without their parent, so they take the parent's access rule, re-rooted onto themselves. You declare security once, on the entity you actually think about. A child may still state its own rule and then stops deriving entirely, so there is always exactly one source for one answer.

preview2 examples compiled by CIsecurityauthorizationcapabilities

Summary#

A platform shape often decomposes into more than one table. A schedule owns its rules; a rule owns its times, weekdays and month-days; a business-hours calendar owns its windows and exceptions. Those child rows have no independent life — a time-of-day that belongs to no rule is not a thing.

So they derive their access: the parent's rule, re-rooted onto the child across the reference that owns it. You declare security once, on the entity you actually have an opinion about.

Signature#

// ONE decision. The rules, their times/weekdays/month-days, and the exclusions all follow it.
partial entity Schedule { security { allow read, create, update, delete when IsAuthenticated; } }

Description#

Why the count of tables is not a count of decisions#

Before this, an app declaring one nightly schedule wrote six near-identical blocks, and a business-hours calendar cost three. That number followed how many tables the shape happens to decompose into — an implementation detail of the shape, not a question anyone was answering six times. It also grew: every table a platform shape gained was a block every app had to add, and the one people forgot was the last one.

A row that is part of another row is governed by that row. Whoever may read a schedule may read its rules; whoever may edit it may edit them. A rule readable by someone who cannot read its schedule is a mistake far more often than a policy.

It reaches all the way down#

Derivation follows the whole chain, not one level. A ScheduleRuleTime is part of a ScheduleRule, which is part of a Schedule — so the rule you wrote on the schedule reaches the time-of-day two hops away. That matters because most of the tables in a shape are usually grandchildren; stopping at one level would leave most of the ceremony in place.

The row filter comes too#

The parent's where clause is not dropped on the way down — it is re-rooted, so it keeps meaning the same thing:

// on the parent
partial entity Schedule { security { allow read where Owner == user; } }

// what the child gets, in effect — the same question asked through the hop
//   a ScheduleRule is readable when   Schedule.Owner == user

A predicate the platform cannot re-root this way is a compile error, never a weakened child rule. That direction is deliberate: a derived rule that quietly dropped a clause would grant more than the parent does.

Declaring your own rule replaces it#

A child that declares its own partial entity … { security { } } uses that and derives nothing:

partial entity Schedule     { security { allow read, create, update, delete when IsAuthenticated; } }
partial entity ScheduleRule { security { allow read when IsAuthenticated; } }   // read-only, and it does NOT also derive

This is the opposite of how capability-owned rows compose, and the difference is load-bearing. There, your block lands alongside the capability's rule and the grants combine — you can add a support role, and you cannot take the owner's access away. Here your block replaces the derivation. If it combined, the stricter rule above would be pointless: the derived delete grant would OR with it and hand back exactly what you just refused.

The rule of thumb follows from that. Reach for an override when a child genuinely differs from its parent — and expect that to be rare, because a child that needs its own policy is usually telling you it is not really part of anything.

Which tables these are#

You do not have to track them. osy explain reports each one's posture as part-of derived and names the entity whose rule governs it, so the answer is always available from the app rather than from a list to keep in your head.

A principal check is COPIED; a row filter is HOPPED#

The two halves of a rule derive differently, and the difference decides what is even possible.

A when asks about the CALLER. when IsFinance — where IsFinance is RoleGrant.Any(g => g.User == user && g.Role == Role.Finance) — never mentions the parent's row at all, so there is nothing to re-root: the child gets the same predicate, unchanged, and grants exactly what the parent grants.

A where is a row filter. where Owner == user reads the parent's own column, so it is re-rooted across the reference — the child's copy reads Schedule.Owner == user.

// Copied verbatim onto every child: it asks about the caller, not about the row.
partial entity Schedule { security { allow read when IsFinance; } }

// Re-rooted onto every child: it reads the schedule's own column.
partial entity Schedule { security { allow read when IsAuthenticated where Owner == user; } }

A collection hop derives too — this is the normal multi-tenant shape#

A where that reads the parent's row through a collection, not a plain column, derives exactly the same way: where Shares.Any(s => s.User == user) re-roots to where Schedule.Shares.Any(s => s.User == user) on the child — the hop's own correlation keeps reading the parent's row, just one reference further out.

This is not a rare shape — it is what almost every multi-tenant where looks like. A row-scoped policy call such as where IsMember(Organization) or where IsAdmin(Organization) — the ordinary way an app scopes a row to the caller's organisation — expands to exactly this: Membership.Any(m => m.User == user && m.Organization == o), a collection hop correlated back to the row's own Organization column. So a [PartOf] child of an entity secured this way — the commonest entity shape in a multi-tenant app — derives correctly, including through a Markdown field, whose sections are exactly this kind of child.

entity Organization { [Required, MaxLength(120)] string Name; security { allow read when IsAuthenticated; } }
enum OrgRole { Member, Admin }
[Principal] entity Person { [Required, MaxLength(255)] string Email; security { allow read when IsAuthenticated; } }
entity Membership {
  [Required] Organization Organization;
  [Required] Person Person;
  OrgRole Role = OrgRole.Member;
  security { allow read when IsAuthenticated; }
}

policy IsMember(Organization o) => Membership.Any(m => m.Person == user && m.Organization == o);
policy IsAdmin(Organization o)  => Membership.Any(m => m.Person == user && m.Organization == o && m.Role == OrgRole.Admin);

// The Markdown field's sections derive from THIS rule — a member reads them, an admin edits them, nobody else does.
entity Handbook {
  [Required] Organization Organization;
  Markdown Text;
  security {
    allow read where IsMember(Organization);
    allow update where IsAdmin(Organization);
  }
}

Only the shape above — a plain Coll.Any(predicate) existence check — derives. A hop that also sorts, pages, projects columns or reaches a second source is refused at compile time rather than derived incorrectly: nothing a security rule mints today needs any of those, so hitting the refusal means the rule is doing something the derived child cannot safely inherit yet. Rewrite the parent's filter to read only what a plain existence check needs, or declare the child's rule yourself where the child is one you can name.

A role check in a when is fine and always was intended to be — it is the commonest rule there is, and it is copied rather than hopped precisely because it has no row correlation to carry.

Examples#

A nightly digest, in full. One security decision, and everything the schedule decomposes into follows it:

[Principal] entity Operator {
  [Required, MaxLength(80)] string Email;
  security { allow read, create when IsAuthenticated; }
}

entity DigestRun { }

// The ONE block. `ScheduleRule`, `ScheduleRuleTime`, `ScheduleRuleWeekday`, `ScheduleRuleMonthDay` and
// `ScheduleExclusion` all take this rule; none of them needs a block of its own.
partial entity Schedule { security { allow read, create, update, delete when IsAuthenticated; } }

void SeedNightly() {
  var s = new Osysharp.Scheduling.Schedule {
    Name          = "Nightly digest",
    Template      = new DigestRun { },
    Zone          = "Europe/Stockholm",
    EffectiveFrom = new DateTime(2026, 1, 1),
  };
  var rule = new Osysharp.Scheduling.ScheduleRule { Schedule = s, Every = ScheduleFrequency.Day, Interval = 1 };
  new ScheduleRuleTime { Rule = rule, At = TimeSpan.FromHours(2) };
}

See also#

Related

secure by default (deny-all)

Deny-all is the posture, and it is the only one: an entity that declares no `security { }` block is denied to every…

security { }

The rules that decide who may read and write an entity's rows. A where clause filters by the row (the owner sees their…

capability rows that belong to a user

Some capability tables hold rows that belong to one signed-in user — a chat conversation is yours, not the app's. Those…

Schedule (recurring work)

A schedule produces work on a cadence. Each occurrence creates a row of the entity its Template names — carrying…