# Osy# — the reference and the specification, as one Markdown file

> Generated 2026-09-15 from the same sources as https://osysharp.com. Each page below is at the URL in its heading; its fenced examples declare how they are verified (`test`/`run` are compiled by CI; `syntax` is a fragment; `preview` is a surface not built yet).



---

<!-- https://osysharp.com/spec/01-scope-and-notation/ -->

# §1 · Scope, conformance, and notation

> **Status: DRAFT.** Claims marked ⚑ were probed against the compiler; the probe is named. Claims marked
> **UNVERIFIED** were not, and must not be relied on.

## 1.1 Scope

This document specifies the Osy# language: its lexical structure, types, declarations, expressions, and the
semantics a conforming implementation must give them.

Osy# is unusual in three ways that the specification must cover explicitly, because a reader arriving from C# will
assume otherwise:

1. **A program spans two execution sides.** One source unit compiles into code that runs in a browser and code that
   runs on a server. Which side a member runs on is **inferred** (§6), not declared.
2. **Execution is durable.** A call may suspend and resume in a different process (§9). This is not a library
   facility; it changes what an expression means, and it is why some C# constructs are refused (§11).
3. **Authorization is part of the language.** Access rules are declared on a type and enforced beneath every read
   and write (§8). No program text can bypass them.

The runtime, the toolchain (`osy …`), the bundled UI kit, and the platform's hosted services are **outside**
this scope. They are specified where they change the meaning of a program, and only there.

## 1.2 Conformance

A **conforming implementation** accepts every program this specification defines as valid, and rejects every
program it defines as invalid, with a diagnostic.

⚠ **Diagnostics are part of the contract, not an implementation detail** (§12). Osy# refuses a great deal on
purpose, and a refusal that does not say what to write instead is a defect in the implementation, not a matter of
taste. Where this specification defines a refusal it also states what the diagnostic must convey.

## 1.3 What is normative

Normative text uses **MUST**, **MUST NOT**, **MAY** in the usual sense.

Everything else — rationale, comparisons to C#, notes on why a rule exists — is **informative**. It is kept
because for this language the reasons are frequently the useful part, but a conforming implementation is judged
only against the normative text.

## 1.4 Notation

Grammar is given in a bracketed EBNF:

| form | meaning |
|---|---|
| `x?` | optional |
| `x*` | zero or more |
| `x+` | one or more |
| `x \| y` | alternatives |
| `( … )` | grouping |
| `'text'` | a literal token |
| *italic* | a non-terminal defined elsewhere in this document |

⚠ **The grammar in this document is written FROM the parser, and is not itself the parser.** Where the two
disagree the parser is correct and this document has a bug — report it rather than working around it. Sections
whose grammar has been checked against a probe say so; sections that have not are marked **UNVERIFIED**.

## 1.5 How claims in this document are established

Every normative claim is expected to carry one of:

- ⚑ **a probe** — the smallest program that distinguishes the claim, and what `osy validate` answered;
- **a test** — a named test class that asserts the behaviour, for anything `validate` cannot see (runtime
  semantics, durability, security enforcement);
- **UNVERIFIED** — stated but not established. Treat as a lead.

This is not ceremony. Three documents in this repository were deleted in the month before this specification was
started, each for confidently describing surface the language no longer had. The difference between those and this
one is meant to be the probe.


---

<!-- https://osysharp.com/spec/02-lexical-structure/ -->

# §2 · Lexical structure

> **Status: DRAFT.** The keyword set (§2.3) is probed. Literals and operators are **UNVERIFIED** and marked so.

## 2.1 Source text

An Osy# source file is Unicode text with the extension `.osy`.

⚑ **A generated migration is NOT Osy# and does not use this extension.** Migrations are `<name>.migration`, in
their own grammar, and are specified in §13. The extension is deliberate: a migration carried the `.osy` extension
until 2026-08-26 and was swept up by every `*.osy` glob — including globs outside this project, which is why the
fix is an extension and not a filter.

## 2.2 Comments

```
// line comment, to end of line
/* block comment */
/// documentation comment, attached to the following declaration
```

Documentation comments are **not** decoration: they are carried into the model and served by `osy docs` and the
language server. **UNVERIFIED**: whether a `///` in a position with no following declaration is an error.

## 2.3 Keywords

⭐ **Osy# has a SMALL reserved set and a LARGE contextual vocabulary, and this is the first thing that surprises a
reader.** Exactly **22** words are reserved. Every other word that looks like a keyword — `entity`, `component`,
`render`, `workflow`, `state`, `event`, `subscribe`, `security`, `allow`, `deny`, `policy`, `terminal`, `live` —
is **contextual**: recognised by the grammar at the position where it means something, and an ordinary identifier
everywhere else.

### 2.3.1 Reserved words

These MUST NOT be used as identifiers:

```
async     await     break     catch     continue  else      false     finally
foreach   if        in        is        new       not       null      return
throw     true      try       var       void      while
```

⚑ **Probe (established, and RUN BY CI).** A reserved word used as an identifier is refused. Source of the set:
the lexer's `Keywords` table, `OsySharpLexer.cs:94`.

```osy probe=refuses PARSE_ERROR
int Probe() { var new = 1; return new; }
```

### 2.3.2 Contextual words

A word that introduces a declaration is not reserved, and MAY be used as an identifier.

⚑ **Probe (established, and RUN BY CI).** Every one of these is a declaration keyword somewhere in the
grammar, and an ordinary identifier here:

```osy probe=accepts
int Probe() {
  var entity    = 1;
  var component = 2;
  var render    = 3;
  var state     = 4;
  return entity + component + render + state;
}
```

*Informative.* This follows C#, which reserves a fixed set and makes later additions contextual so that adding a
word to the language cannot break existing programs. Osy# takes the same position, and takes it further: the words
that introduce its most distinctive constructs are all contextual, so a program that used `workflow` as a variable
name before workflows existed still compiles.

⚠ **Consequence for tooling, and for readers:** you cannot decide what a word means by looking it up in a list.
`state` is a declaration keyword inside a `workflow` body and a variable name in a function body. Syntax
highlighting that colours it unconditionally is wrong, and so is a mental model that treats these as reserved.

### 2.3.3 Words that are NOT in the language

**UNVERIFIED as a complete list**, but recorded because each has been mistakenly assumed to exist:

| assumed | actual |
|---|---|
| `out`, `ref` | **refused** — a parameter passes a value in, and only the return comes back out. A call can suspend and resume elsewhere, so no caller frame is guaranteed to still be waiting for a write-back. See §11. |
| `[Display]` | replaced by `[Label]` |
| `Now` | removed entirely; use `DateTime.UtcNow`, which the compiler lowers to the ambient clock |

## 2.4 Identifiers

**UNVERIFIED.** Expected to follow C# — a letter or `_` followed by letters, digits or `_`, compared ordinally.
The probe that would establish this (a Unicode identifier, a leading digit, `@`-escaping) has not been written.

Members are `PascalCase` and parameters `camelCase` by convention; this is style, not grammar, and the linter —
not the compiler — enforces it.

## 2.5 Literals

**UNVERIFIED.** The forms below are observed in real source but their exact grammar has not been probed:

| kind | observed |
|---|---|
| integer | `1`, `0` |
| decimal | `2m` — a suffixed decimal literal |
| string | `"…"` and interpolated `$"…{expr}…"` |
| boolean | `true`, `false` |
| null | `null` |

## 2.6 Operators and punctuation

**UNVERIFIED as a complete table.** Two facts are established elsewhere and belong here when this section is
written properly:

- ⚑ `??` over numeric operands **WIDENS** to the wider type, rather than dropping to the narrower one. The
  previous rule produced wrong numbers, not merely a dropped cast (fixed 2026-08-26, `f15275418`).
- ⚑ `xs[i]` on a collection is **not** a nullable read — the resolver briefly said it was, and a MUST-tier lint
  believed it (fixed 2026-08-26, `8ed45bfbd`).

---

**Next:** §3 Types — the type system, nullability, and the required-by-default rule (R12). Not started.


---

<!-- https://osysharp.com/spec/06-execution-side/ -->

# §6 · The execution-side model

> **Status: DRAFT.** The side vocabulary and one inference are probed. The *correctness* of inference at runtime,
> and what crosses the wire, are **UNVERIFIED here by construction** — see §6.6.

This is the language's most distinctive rule, and the one §11.1 and §11.2 both depend on.

## 6.1 One unit, more than one runtime

A single compilation unit produces code that runs in a browser and code that runs on a server. There is no wire
protocol to declare, no endpoint to route, and no data-transfer type to keep in step, because there is no boundary
in the source for those things to sit on.

**Normative.** A program MUST NOT be required to state where a member runs. An implementation MUST determine it.

## 6.2 The three sides

⚠ **The model is three-valued, not two.** A reader who assumes "client or server" will mispredict the third and
most interesting case.

| side | meaning |
|---|---|
| **Server** | the authority for data **reads**, security, and secrets — the server interpreter runs it |
| **Client** | dialogs, local UI, interaction — the client runtime runs it |
| **Either** | pure compute **and local data mutation** (`new`, assignment) — runs wherever the cursor already is, and **may span the boundary** |

*Source: `ExecutionSide`, `OsySharpExpressionEnums.cs:539`.*

⭐ **`Either` is where the model earns itself.** A pure helper does not need a home, so it does not get one — it runs
on whichever side called it, and the same function called from a render slot and from a server function is not two
functions. Local mutation is `Either` for the same reason: creating a row or assigning a field acts on the unit of
work the cursor already holds, so it is meaningful on both sides and identical on both.

## 6.3 Inference

The side follows from **what the code touches**, not from what it is called or where it is written.

- an **entity read** materialises rows under the declared row filter (§8) → **Server**
- a **render** expression paints and performs no I/O → **Client**
- **pure compute** and local mutation → **Either**

⚑ **Probe (established, and run by CI).** A `live var` holding an entity read is reported `side=server`:

```osy probe=accepts
using Osysharp.Ui;
entity Order { [MaxLength(40)] string Ref; int Total; }
[Page("/")] [AllowAnonymous] component Board() {
  live var orders = Order.Where(o => o.Total > 0);
  render { foreach (var o in orders) { Text(o.Ref); } }
}
```

`osy model --json` reports, for that component member:

```
component Board
   members: orders  side=server
```

## 6.4 Asking the compiler

**Normative.** An implementation MUST make the resolved side inspectable. A rule the author cannot observe is
folklore.

⚠ **Two different answers, and they are easy to confuse:**

| you have | `osy model --json` gives you |
|---|---|
| a **component member** | a `side` — `server` / `client` / `either` |
| a **top-level function** | an **`effects`** record — `readsData`, `writesData`, `pure`, `durability`, plus the entities read/created/modified — and its `transitive` closure |

⚑ **Probed.** A top-level function has **no `side` field**; its keys are `allowAnonymous · effects · file ·
isSystem · name · parameters · returnNullable · returns · scopedName · transitive · visibility`. The effects record
is the answer for a function — `OpenOrders` reports `reads: [Order], readsData: true, pure: false`.

*Informative.* The distinction is not an oversight. A component member is bound to one side because it is part of a
rendered tree; a top-level function is frequently `Either` and takes its side from the caller, so "what does it
touch" is the useful question and "where does it run" is not always answerable in isolation.

## 6.5 Forcing the side

`[Server]` and `[Client]` exist. They force a member onto a side.

**Reaching for one is almost always the wrong instinct.** They were de-advertised across **718 mentions in 174
files** on 2026-08-27, and removed from `osy init`'s scaffold — the first Osy# a downloader reads — precisely
because their presence in examples taught readers to write them. The scaffold's explanatory comment was also
false: it claimed a plain function runs on the server and a render expression cannot call one; measured, a plain
pure top-level function *is* callable from a render.

**Normative.** An implementation MUST NOT require these attributes for ordinary programs, and diagnostics MUST NOT
suggest adding one as the remedy for an inference the author has not been shown.

## 6.6 What this section does NOT establish

**UNVERIFIED, deliberately.** `osy validate` — the instrument behind every probe in this document — establishes
what the compiler **accepts** and what it **reports**. It cannot establish:

- that an inferred side is **correct at runtime** (that the server half really runs on the server);
- **what crosses the wire** at a hand-off, and in which direction;
- that a value which cannot cross is refused rather than silently dropped.

Those are runtime semantics and need a test class, per §1.5. `ExecutionSidesHandlerTests` is the closest existing
guard and is named here so the gap is addressable rather than merely admitted. **A future revision of this section
MUST cite tests for the three claims above, or drop them.**

---

**Next:** §9 Durability semantics — the other half of the model §11 depends on. **Drafted.**


---

<!-- https://osysharp.com/spec/09-durability/ -->

# §9 · Durability semantics

> **Status: DRAFT.** The classification, its lattice, and where the compiler places a durable step are probed —
> the first two through `osy model --json`, the third read from the resolver that performs the lowering. What a
> **crash** actually does is runtime behaviour and is **UNVERIFIED here**; §9.7 names the guards.

§11.1 and §11.2 both refuse a C# construct "because a call can suspend and resume in a different process". This
section is that claim, stated properly. It is the reason the language has no `out` parameter and no `async`.

## 9.1 The premise

**Normative.** A call MUST NOT assume that the process which began it is the process which finishes it.

Everything below follows from that one sentence. An Osy# call may suspend — for a timer, for a human, for a child
workflow — and resume later, elsewhere. So the stack frame is not a durable place: nothing may be written back into
it (§11.1), and nothing may be scheduled against it (§11.2).

⭐ **The interesting consequence is that a re-run is normal, not exceptional.** A continuation-based engine replays
nothing wholesale, but it does re-execute in two places: a statement is replayed after an in-process sub-call
returns, and a statement that suspended is re-entered on resume. So "what does it cost to run this leaf twice" is a
question the engine must be able to ask of every leaf in the program — which is what §9.2 is.

## 9.2 The durability classification

Every leaf the language provides carries one of three values.

| value | a re-run… | so it is |
|---|---|---|
| **Deterministic** | reproduces its value, and nothing escaped | free — never recorded |
| **Nondeterministic** | yields a *different* value, but nothing left the platform | memoized on the per-statement node memo |
| **External** | is a **second real-world effect** — a second charge, a second enqueue | given its own durable step |

*Source: `StdlibDurability`, `Platform.Core/Dsl/OsySharp/Stdlib/OsySharpStdlib.cs`.*

**Normative.** The three values form a **total order** — `Deterministic < Nondeterministic < External` — and the
durability of a construct MUST be the maximum over its parts. That is what makes the classification usable as a
lattice over whole programs rather than only over the leaves it was defined on.

⚠ **`External` is about *whose* system rolls back, not about I/O.** A commit to the application's own database is
`Nondeterministic`: the platform owns the transaction. `File.WriteAllText` is `External`: the filesystem does not
roll back because a later segment failed.

## 9.3 Asking a program what it costs

**Normative.** An implementation MUST report the classification, and MUST report a witness for it.

⚑ **Probed 2026-08-27.** For a function whose body is `Security.RandomId()` and then `File.WriteAllText(path, id)` —
a `Nondeterministic` leaf written *first* and an `External` leaf written *second* — `osy model --json` reports:

```
Archive   direct: External   durabilityVia = File.WriteAllText
Wrapper   direct: Deterministic          via = (none)
          trans : External   durabilityVia = Archive → File.WriteAllText
```

Three separable facts, each of which a reader would plausibly guess wrong:

1. **The lattice wins over source order.** `External` is reported although the nondeterministic call is written
   first. The value is a max, not a last-writer.
2. **`durabilityVia` is a path, not a name.** Through a call graph it reads `Archive → File.WriteAllText` — the
   route by which a plain-looking call reaches an egress. This is the field that makes the classification
   *actionable* rather than merely present.
3. **`effects` and `transitive` are different answers and both are given.** `Wrapper` calls nothing but `Archive`:
   it is `Deterministic` **directly** and `External` **transitively**. A reader who consults only one of them will
   be wrong about half the functions in any real program.

## 9.4 Where a durable step goes — the leaf, never the function

**Normative.** An implementation MUST place the exactly-once boundary at the **egress leaf**, not at the enclosing
function.

⭐ **This is the load-bearing design decision of the whole section.** If `Notify(u)` makes external calls A and then
B, and the whole of `Notify` is one coarse step, a crash between A and B re-runs A on resume — a double send. Each
egress is therefore its own step, and the deterministic orchestration between them re-runs freely, being a pure
function of the memoized results.

**It is automatic.** The resolver wraps every `External` leaf in a durable step during lowering. An author writes
`Http.Get(url)` and gets exactly-once without naming it. `Workflow.Once("key", …)` remains available for a step the
author wants to name or key, and an already-stepped leaf is not wrapped twice.

⚠ **So the transitive lattice of §9.3 does not decide anything.** It is an *author-facing* signal — "this
plain-looking call reaches an egress". Step placement is a purely **local** decision at each leaf. Reading the
lattice as the mechanism gets the model backwards, and is the likeliest misreading of this section.

**A nondeterministic leaf gets no step, deliberately.** `DateTime.UtcNow` and `Guid.NewGuid` ride the per-statement
node memo, which already serialises into the continuation. Giving each one its own out-of-band database write would
be a real cost for a guarantee they already hold. Only egress — where the second execution reaches *someone else's*
system — earns one.

⚑ **An unclassified target defaults to `External`.** A leaf missing from the durability table is treated as egress,
so an omission over-protects rather than under-protects. *Informative:* the platform has been bitten by the
resulting symptom more than once, which is itself the argument for the default being this way round.

## 9.5 A step wraps one thing

```osy probe=accepts
int Charge(int amount) { return Workflow.Once("charge", () => amount * 2); }
```

```osy probe=refuses RESOLVE_ERROR
int Charge(int amount) { return Workflow.Once("charge", () => { var a = amount; return a; }); }
```

A step body is an **expression**. Statements go in a function, which the step then calls.

⚠ *Informative, and honest about a rough edge:* the refusal above is real and stable, but its diagnostic explains
itself in terms of **query** lambdas ("a block can't lower to SQL in a query predicate") because block-bodied
lambdas are refused language-wide from one site. At a durable-step call site that reason is not the applicable one.
The refusal is right; the explanation is aimed elsewhere. Recorded as a §12 diagnostics concern.

## 9.6 `await` means *park*

Restating §11.2 from this side, because it only makes sense here: `await Workflow.Run(…)` does not yield a thread.
It **parks the run durably** — possibly for days — and resumes it in whatever process next picks it up. The keyword
is reused because the English reading is right; the mechanism is this engine, not a task scheduler.

## 9.7 What this section does NOT establish

**UNVERIFIED, deliberately.** Every claim above is about what the compiler *classifies* and *lowers*. None of it
establishes what a crash does. Specifically unproven here:

- that a step's result survives a process restart and is not re-executed on resume;
- that a memoized nondeterministic value is the *same* value after resume;
- that a durable step commits out-of-band, so a crash before the enclosing segment commits still finds it.

Those need runtime tests. The guards that exist are `AutoDurableLoweringTests` (the leaf-level wrap),
`WorkflowOnceDurableStepTests` and `WorkflowStepMemoIdentityTests` (memo identity across a resume),
`DurableFunctionRunnerTests` (resume itself), and `FunctionEffectsDurabilityTests` with
`OsySharpStdlibDurabilityTests` (the classification and its rollup). **A future revision MUST cite which of them
proves which of the three claims above, or drop the claim.**

---

**Next:** §8 Security as a language rule — the other declaration-time rule, and the one with the most surface.
Not started.


---

<!-- https://osysharp.com/spec/11-non-features/ -->

# §11 · Deliberate non-features

> **Status: DRAFT.** Every refusal below is probed and run by CI (`SpecProbeGateTests`). The *reasons* are
> informative; the refusals are normative.

Most specifications list what a language has. This section lists what Osy# **refuses**, and why — because in every
case here the reason is a property of the execution model rather than a matter of taste, and a reader who knows the
reason can predict the next refusal instead of discovering it.

⚠ **These are not "not yet".** Each is a decision. Where a construct might arrive later, this section says so
explicitly; where it cannot, it says that too.

---

## 11.1 `out` and `ref` PARAMETERS

**Normative.** A parameter declaration MUST NOT carry `out` or `ref`.

```osy probe=refuses PARSE_ERROR
int TryIt(string s, out int value) { value = 1; return 0; }
```

**Why, and it is the model rather than the taste.** A parameter passes a value **in**; only the return comes back
out. An Osy# call can **suspend and resume in a different process** (§9), so there is no caller frame guaranteed to
still be waiting for a write-back when the callee finishes. `out` is not merely unidiomatic here — its meaning
depends on a stack frame the execution model does not promise to keep.

⚑ **A declaration cannot arrive later.** Its signature is the thing the durability model forbids, so this half is
excluded by §9 rather than awaiting demand. Recorded as an owner decision, 2026-08-26.

### 11.1a ⛔ CORRECTION — a `TryParse` CALL SITE compiles, and this section said it could not

**Normative.** `TryTTT(…, out var x)` on a stdlib parser IS accepted.

```osy probe=accepts
decimal Parsed(string s) {
  if (decimal.TryParse(s, out var v)) { return v; }
  return 0m;
}
```

Until 2026-08-29 this section asserted three things that are now false, and they are written out rather than
edited away because a spec is read as authority:

| it said | the truth |
|---|---|
| "A call site MUST NOT pass one" | a stdlib `TryParse` call site may |
| "**there is no `TryParse`**" | there is |
| the `Convert.ToInt` / `int.Parse` table was "the two real forms" | there are three |

⚠ **AND MY OWN GATE PASSED THROUGH THE CHANGE.** `SpecProbeGateTests` is built to fail in both directions —
including on "a refusal the language no longer makes", which the plan calls the more dangerous kind of stale
text. It did not fire, because **the probe was narrower than the claim**: the claim covered a declaration AND a
call site; the probe only ever exercised a declaration. A gate is only as wide as the narrowest thing it
actually runs, and a normative sentence with no probe under it is prose.

⚑ **The lesson for the rest of this document, and it is a rule now:** every clause of a normative sentence needs
its own probe. Two clauses joined by "and" are two claims, and pinning one of them proves nothing about the
other.

### 11.1b How it works, given `out` is refused

**`out` is never accepted — the call is REWRITTEN before resolution.** `decimal.TryParse(s, out var v)` becomes,
hoisted ahead of its statement:

```
decimal? v = null; try { v = decimal.Parse(s); } catch { }
```

with `v != null` left in the call's place. So no `out` reaches the resolved tree: there is no write-back, no
caller frame to survive a suspension, and nothing for §9 to forbid. **The nullable local is the did-it-work
channel** — the shape Osy# already uses everywhere — and `TryParse` is C# spelling the compiler translates into
it.

*Informative — why the two halves differ.* A DECLARATION is a signature the author writes and the durability
model cannot honour. A CALL SITE is a shape the compiler owns end to end, so it can be rewritten out of
existence. The durability argument bites on the first and has nothing to say about the second.

⚑ **And it declines by POSITION, which is the tell that this is a rewrite rather than support.** The hoist needs
somewhere to go, so a loop CONDITION or a lambda BODY is refused — there the parse would have to happen per
iteration or per element rather than once before. The diagnostic prints the whole mechanism and the remedy for
each position, which is why this section can describe it without reading the resolver.

---

## 11.2 `await` on an ordinary call

**Normative.** `await` MUST NOT be applied to an ordinary function call.

```osy probe=refuses RESOLVE_ERROR
int Inner() { return 1; }
int Outer() { var v = await Inner(); return v; }
```

**Why.** Effects run **in place**. There is no async/await colouring in Osy#, because there is nothing for it to
distinguish: a call that performs I/O and a call that does not are written and read the same way, and the runtime
decides how to execute them.

**The one exception is `await Workflow.Run(...)`** — a Saga-style durable wait on a *child workflow* (§9). That is
not the C# meaning of `await`. It does not yield a thread and resume shortly after; it **parks the run durably**,
possibly for days, and resumes it in whatever process picks it up. The keyword is reused because the reading is
right — "continue when that finishes" — but the mechanism is the durable engine, not a task scheduler.

*Informative.* This is why §11.1 and §11.2 belong together: both follow from a call not owning a stack frame for
its own lifetime.

---

## 11.3 `Now`

**Normative.** The identifier `Now` MUST NOT resolve. It is not merely deprecated — the name is absent from
app-source resolution entirely, and answers exactly what any undefined name answers.

```osy probe=refuses RESOLVE_ERROR
DateTime When() { return Now.Utc; }
```

**Write `DateTime.UtcNow`.** The compiler lowers it to the ambient clock, so it is testable (`TestClock.*`) and
correct inside a `live var`, a render slot, and a durable body alike.

⚑ **Why the name is hidden rather than deprecated.** A retirement *message* was tried first and made things worse:
`Osysharp.Now` is a real class in the model, so with the refusal removed `Now.Utc` resolved far enough to answer
*"'Osysharp.Now.Utc' is an instance member"* — which still tells the reader the class is there. Worse, a typo'd
`Nowt` produced *"Did you mean 'Osysharp.Now'?"*: the compiler proactively recommending a name it would then refuse.
Hiding the name from resolution is what makes the removal complete.

---

## 11.4 A retired spelling is deleted, not aliased

**Normative.** A renamed surface MUST NOT keep its old spelling as an alias.

| retired | current |
|---|---|
| `[Display]` | `[Label]` |
| `Modes(light:, dark:)` | `Modes.Of(light:, dark:)` |
| `<hash>.migration.osy` | `<hash>.migration` |

*Informative.* Osy# has no released versions and no deployed programs outside this repository, so an alias would
preserve compatibility with nothing while doubling the surface a reader must learn and a diagnostic must consider.
The rule is expected to change when the language ships a stable release; §13 will say how.

---

## 11.5 What is NOT in this section

Two things are frequently assumed to be deliberate refusals and are not:

- **`[Server]` / `[Client]` annotations.** These exist. They are a forcing hatch, and reaching for one is almost
  always the wrong instinct — the side is inferred (§6). They were de-advertised across 174 files on 2026-08-27
  precisely because their presence in examples taught readers to write them. Absent from your source is the
  normal case, not a refusal.
- **A scalar array on an entity** (`string[] Tags;`). Refused today, with a diagnostic naming the two real options
  — but this is a **gap**, not a decision, and it is tracked as such. **UNVERIFIED** here: no probe written.

---

**Next:** §6 The execution-side model and §9 Durability semantics — the two rules §11.1 and §11.2 both
depend on. **Both drafted.**


---

<!-- https://osysharp.com/reference/agent/index/ -->

# Agents (calling a model like anything else you declared)

> An `agent` is a declaration — instructions, tools, model — and calling it is calling a name. The first-day mistake is treating the call as a chat: you hand it the turns you want it to see, so what it remembers is what you passed, and every run is recorded as a task with its cost and outcome whether you look or not.

<!-- id: agent-index · area: agent · stability: stable · html: https://osysharp.com/reference/agent/index/ -->

## Summary        {#summary}
**An agent is something you declare, and calling it is calling a name.** Its declaration supplies its
instructions, its tools and its model, so the call site stays about what you are asking rather than about how the
model is configured.

```osy title="the call is a call" test app=agent-index
using Osysharp.Agents;

[Principal] entity User {
  [Required, MaxLength(100)] string DisplayName;
  security { allow read when IsAuthenticated; allow create when IsAuthenticated || IsAnonymous; }
}

agent Support {
  Purpose   = "Answer questions about an order.";
  Prompt    = "You answer order questions from the context you are given.";
  Principal = new User { DisplayName = "Support" };
}

string Answer(string question) {
  var turns = new List<Turn>();
  turns.Add(Turn.User(question));
  return Support.Ask(turns).Text;
}
```

## Description    {#description}
**You hand it the turns it should see.** There is no hidden session: the question, the history worth replaying and
the context you assembled are the argument ([running an agent from your code](https://osysharp.com/reference/agent/ask/)). What it "remembers" is what you passed — see
[agent conversation memory (using Osysharp.Agents)](https://osysharp.com/reference/agent/conversation-memory/) for the shapes that are worth passing and the ones that are not.

**Every run is a task, recorded.** Cost, duration and outcome are captured whether or not you look
([the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/)). [watching a task run (task.Watch)](https://osysharp.com/reference/agent/task-watch/) follows one in flight, [what the agent saw (task.Transcript)](https://osysharp.com/reference/agent/task-transcript/) reads what it actually
said and did, [what a task cost, and what it did (task.Calls)](https://osysharp.com/reference/agent/task-calls/) lists the tools it reached for, and [stopping work (task.Stop)](https://osysharp.com/reference/agent/task-stop/) ends one.

**The model is a declaration, not a call-site argument.** [app.Models — the models an app admits, each with a name](https://osysharp.com/reference/agent/models/) is how a model is named and
[default LLM model (app.DefaultModel)](https://osysharp.com/reference/agent/default-model/) what a call gets when it names none — so changing which model an agent uses is an edit in
one place, not a sweep of call sites.

**A long-running agent is a workflow, not a loop you write.** [the agent loop (app.Agent, Loop)](https://osysharp.com/reference/agent/loop/) is the shape for repeated turns, and
[an agent asking a person (the human slot)](https://osysharp.com/reference/agent/hitl/) is how a person is brought into one — which is a parking point, with everything the workflow area
says about surviving a deploy. [what an agent hands back (AgentDeliverable)](https://osysharp.com/reference/agent/deliverables/) is how a run's output becomes something the app holds.

## The pages      {#the-pages}
Run `osy docs agent` for the full listing.

- **Calling one** — [running an agent from your code](https://osysharp.com/reference/agent/ask/), [agent conversation memory (using Osysharp.Agents)](https://osysharp.com/reference/agent/conversation-memory/), [the agent loop (app.Agent, Loop)](https://osysharp.com/reference/agent/loop/)
- **Which model** — [app.Models — the models an app admits, each with a name](https://osysharp.com/reference/agent/models/), [default LLM model (app.DefaultModel)](https://osysharp.com/reference/agent/default-model/)
- **Turning it off** — [turning AI off (the runtime switch)](https://osysharp.com/reference/agent/ai-switch/), the operator's runtime disable
- **Capping it** — [LLM budgets (hard daily limits per organisation, app and user)](https://osysharp.com/reference/agent/llm-budget/), hard daily budgets per organisation, app and user, enforced before the call leaves
- **People in the loop** — [an agent asking a person (the human slot)](https://osysharp.com/reference/agent/hitl/), [what an agent hands back (AgentDeliverable)](https://osysharp.com/reference/agent/deliverables/)
- **Watching a run** — [the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/), [watching a task run (task.Watch)](https://osysharp.com/reference/agent/task-watch/), [what the agent saw (task.Transcript)](https://osysharp.com/reference/agent/task-transcript/),
  [what a task cost, and what it did (task.Calls)](https://osysharp.com/reference/agent/task-calls/), [stopping work (task.Stop)](https://osysharp.com/reference/agent/task-stop/)

## See also   {#see-also}
- [running an agent from your code](https://osysharp.com/reference/agent/ask/) — the call, and what a turn list is for
- [the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/) — what every run records, without being asked
- [an agent asking a person (the human slot)](https://osysharp.com/reference/agent/hitl/) — bringing a person in, and why that makes it a parking point


---

<!-- https://osysharp.com/reference/agent/llm-budget/ -->

# LLM budgets (hard daily limits per organisation, app and user)

> Every model call an application makes is metered, and the platform refuses a call before it leaves when the day's budget is spent. Budgets are set by the organisation's owner in the admin — per organisation and per application, in tokens and optionally in dollars — and per user through an app-local override. A budget that cannot be verified refuses too (fail-closed). A refused call fails with a message naming the scope, the limit and the usage; it never silently returns nothing.

<!-- id: agent-llm-budget · area: agent · stability: stable · html: https://osysharp.com/reference/agent/llm-budget/ -->

## Summary        {#summary}
Beside the [runtime switch](https://osysharp.com/reference/agent/ai-switch/) sits the dial. The platform meters every LLM and embedding call an
application makes and enforces **hard daily budgets before the call leaves**: a call that would exceed the day's
limit is refused, not sent. Nobody wakes up to a bill because an agent looped overnight — the eleventh call over
the cap is refused, and the application carries on without it.

Three scopes, checked in order, any one of which can refuse:

| Scope | Set where | Limits |
|---|---|---|
| **the organisation** | admin → org → Budgets, by the org's Owner (or a platform admin) | daily LLM tokens · daily embedding tokens · an optional daily USD cap |
| **the application** | the same page, per app | the same three, plus the **default per-user daily token limit** for the app |
| **one user of the app** | the app itself, through `UserLlmBudget` (this page's Osy# surface) | that user's daily LLM tokens; `0` blocks them |

The budget day starts at midnight in the budget's own time zone (the organisation's, when set), not the server's.

## Signature      {#signature}
```text
admin → org → Budgets
   Daily LLM tokens        ______   (blank = platform default · 0 = blocked)
   Daily embedding tokens  ______
   Daily USD cap           ______   (blank = no cost cap; the token limit is the floor)
   Default per-user tokens ______   (application rows only)
```

```osy syntax
using Osysharp.Llm.Budget;        // and `use Osysharp.Llm.Budget;` in app.osy

entity UserLlmBudget {          // provided by the capability — one row per user, optional
  principal UserId;             // the app-local user this override applies to
  long DailyLlmTokenLimit;      // input + output tokens per budget day; 0 = blocked
}
```

An application never declares its own organisation or application cap in source: those are the operator's, set
outside the app's reach for the same reason the [switch](https://osysharp.com/reference/agent/ai-switch/) is. What an application *can* do is
narrow the per-user limit below the app default, row by row.

## Description    {#description}

### How a call is metered   {#metering}
Before a model call is sent, the gate **reserves** the estimated tokens against each scope in one atomic statement
— check and increment together, so two concurrent calls cannot both squeeze under a limit. After the response it
**settles** the reservation to the actual tokens. Usage is recorded per scope, per day, per model, so the admin's
budgets page shows today's spend against the cap, and the [audit trail](https://osysharp.com/reference/config/audit/)'s `LlmCallRecord` carries
the same numbers per call.

### What a refusal looks like   {#refusal}
A refused call throws. The message names the scope, the limit and the usage, and says when the day resets:

```text
Daily LLM token budget for this application (2,000,000 tokens) is exhausted (2,000,113 used); resets at 00:00 Europe/Stockholm.
```

Over HTTP the platform answers **429**; in a workflow or a background handler the refusal is terminal for that call
— it is logged and not retried, because a retry storm on an exhausted budget is exactly what a budget exists to
prevent. In Osy# it is an ordinary exception: catch it where the application has a sensible way to go on without
the model — the same degraded path the switch uses.

```osy syntax
try {
  var reply = Screener.Ask(turns);
} catch (Exception e) {
  // budget exhausted, or AI switched off: the report is screened by the rules alone
  report.ScreeningSkipped = true;
}
```

### Fail-closed   {#fail-closed}
If a budget cannot be verified — the admin row cannot be read, the accounting statement fails — the call is
**refused**, with a message that says so rather than one that claims exhaustion. A budget that fails open is not a
budget; measured once on this platform (an incident on 2026-06-12) and the gate has refused on doubt since.

### Per-user overrides   {#per-user}
The application's default per-user limit comes from the admin. An app that wants to give one person more, or block
one, writes a `UserLlmBudget` row — an ordinary entity under the app's own `security { }`, so who may set it is the
app's rule:

```osy title="a per-user override the app's admin may set" test app=agent-llm-budget
using Osysharp.Llm.Budget;

[Role] enum Role { Authenticator, Admin }
entity RoleGrant { [Required] User User; [Required] Role Role;
  security { allow read when IsAuthenticated; allow create when IsAuthenticator; } }
policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == Role.Authenticator);
policy IsAdmin         => RoleGrant.Any(g => g.User == user && g.Role == Role.Admin);

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

// The capability's entity, reopened only to say who may write it: the app's Admin.
partial entity UserLlmBudget { security { allow read, create, update when IsAdmin; } }

void CapUser(User u, long tokensPerDay) {
  var b = UserLlmBudget.Where(x => x.UserId == u).FirstOrDefault();
  if (b == null) { new UserLlmBudget { UserId = u, DailyLlmTokenLimit = tokensPerDay }; }
  else { b.DailyLlmTokenLimit = tokensPerDay; }
}

[AuthMethod] string Login(string email, string password) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  if (u == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); }
  return "";
}
app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };
app.AuthBootstrap = new AuthBootstrap { Login = Login, Role = Role.Authenticator };
```

A user with no row gets the application's default; a row with `0` blocks that user's model calls entirely while
everyone else's continue.

### What it answers for procurement   {#procurement}
The switch answers *can it be turned off*. The budget answers *what is the most it can ever cost us in a day* —
per organisation, per application, per person, enforced by the platform rather than promised by the application,
and visible on one admin page against today's usage.

## Examples       {#examples}

An organisation caps itself at a dollar a day and one application at a fifth of that:

```text title="two caps, and the day they reset in"
admin → org "Acme" → Budgets           daily USD cap 1.00 · zone Europe/Stockholm
admin → org "Acme" → app "Desk" → Budgets   daily USD cap 0.20 · default per-user tokens 50,000
```

The twentieth cent Desk would spend is refused; Acme's other apps keep their share; the person the app's admin
capped at 0 with `CapUser(u, 0)` is refused before the app's cap is ever consulted.

## See also       {#see-also}
- [turning AI off (the runtime switch)](https://osysharp.com/reference/agent/ai-switch/) — the switch this dial sits beside: off in seconds, from the same control plane
- [what a task cost, and what it did (task.Calls)](https://osysharp.com/reference/agent/task-calls/) — where a task's own calls and their cost are read back in the app
- [audit read access (app.Audit)](https://osysharp.com/reference/config/audit/) — `LlmCallRecord`, the per-call trail with the same numbers and its own retention
- [Agents (calling a model like anything else you declared)](https://osysharp.com/reference/agent/index/) — the rest of the agent and model surface


---

<!-- https://osysharp.com/reference/agent/conversation-memory/ -->

# agent conversation memory (using Osysharp.Agents)

> A conversation is stored as a `ChatSession` plus its `ChatMessage` turns, so an agent asked a follow-up can refer back to what was said earlier. Both ride `Osysharp.Agents` — the same capability an agent declaration already needs — so there is nothing extra to opt into. An agent stays stateless until a call names a conversation. A conversation belongs to the user who started it and is readable only by them.

<!-- id: agent-conversation-memory · area: agent · stability: preview · html: https://osysharp.com/reference/agent/conversation-memory/ -->

## Summary        {#summary}
An agent is **stateless by default**. Each turn is answered on its own: nothing from the previous exchange is in
scope unless a call names the conversation it belongs to. That is the right shape for an agent that classifies a
document or answers one question.

A conversation is stored as a **`ChatSession`** with its turns as **`ChatMessage`** rows, and an agent asked a
follow-up can see what came before. Both entities ride `Osysharp.Agents` — the capability an `agent` declaration
already requires — so a conversation needs no second import.

```osy syntax
using Osysharp.Agents;
```

## Signature      {#signature}

```osy syntax
using Osysharp.Agents;
```

Two entities enter the app's model:

| entity | what it holds |
|---|---|
| `ChatSession` | one conversation — which agent, its title, when it was last active, who it belongs to, and optionally the record it is about |
| `ChatMessage` | one turn — a user message, an assistant reply, a tool call or a tool result, in `Sequence` order |

## Description    {#description}

**Conversations are part of what the agent surface is.** They are not a second thing to import: a session and its
turns are what an agent that holds a conversation, parks for a human, or reports an outcome would otherwise
re-implement, so they ride `Osysharp.Agents` itself. Nothing about your agents changes to get them: the same agent
declaration, the same calls.

**Which conversation, if any, is decided per call.** A request that carries a `conversationId` continues that
conversation; one that does not starts a new one. An app whose agents only ever classify a document names no
conversation and its tables stay empty.

**A conversation belongs to one user.** It is stamped with the user who created it, listings return only that user's
conversations, and asking for someone else's by id answers `404` — the same answer as an id that does not exist, so
no one can probe for which conversations exist. Anonymous visitors own no conversations: an anonymous caller gets an
empty list, and creating one requires a signed-in user.

**A long conversation is summarised rather than truncated.** When the history outgrows the model's context window,
earlier turns are replaced by a summary turn, so the beginning of the conversation is still represented instead of
being dropped.

**Entity context is optional.** A conversation can record the record it is about — "ask the agent about *this*
order" — so it can be listed alongside that record rather than in one undifferentiated pile.

## Examples       {#examples}

An app whose agent holds a conversation:

```osy title="conversation-memory-optin" test app=conversation-memory-optin
using Osysharp.Agents;

[Principal] entity User {
  [MaxLength(255)] string Email;
}

entity Order {
  [Required, MaxLength(50)] string Reference;
}
```

A chat request that carries a `conversationId` continues that conversation; one that does not starts a new one.

## See also       {#see-also}
- [default LLM model (app.DefaultModel)](https://osysharp.com/reference/agent/default-model/) — the model an agent talks to.


---

<!-- https://osysharp.com/reference/agent/hitl/ -->

# an agent asking a person (the human slot)

> An agent that needs something only a person can supply does not guess and does not fail — it asks, and its work PARKS. The wait becomes a modelled fact with an assignee and a clock, not a paused process. Hours later the answer arrives as the result of the question it asked, and the same work carries on. Asking is available to any agent running inside a loop, and nothing is declared to enable it.

<!-- id: agent-hitl · area: agent · stability: preview · html: https://osysharp.com/reference/agent/hitl/ -->

## Summary        {#summary}

An agent working on something real will sometimes reach a point only a person can settle. A charge nobody can
explain from the record. A judgement that is somebody's to make. The two obvious ways out are both bad: guessing
produces a confident wrong answer, and failing throws away the work already done.

So it asks. The run **parks**, the question goes in front of a person, and when they answer, the work continues from
exactly where it stopped.

```osy title="the ask that parks — branch and go wait" syntax
var reply = Auditor.Ask(turns);

if (reply.Parked) {
  // `reply.Question` is what it wants to know. Put it in front of somebody.
  goto Waiting;
}
```

…and later, when they have replied:

```osy title="hours later, the answer resumes the same run" syntax
var reply = Auditor.Answer(this.Item, "A client dinner for four — I paid for all.");
```

## Signature      {#signature}

```osy syntax
<agent>.Answer(task, text)      // → AgentReply
```

| | |
|---|---|
| `task` | the task whose run is parked — inside a loop that is `this.Item`. Must be an `AgentTask` or one of your types deriving from it |
| `text` | what the person said, verbatim. It reaches the agent as the answer to the question it asked |
| returns | an `AgentReply`, exactly as [`Ask`](https://osysharp.com/reference/agent/ask/) does — **because a resumed run can park again** |

## Description    {#description}

### Asking comes from the loop, and is declared nowhere   {#capability}
An agent running inside a [loop](https://osysharp.com/reference/agent/loop/) can ask a person. An agent that is not, cannot — the verb is not
offered to it at all.

That is not a restriction, it is the only honest answer. Parking means *"stop here and come back when somebody
replies"*, and coming back is the loop's doing: a loop is checked at compile time to declare a `Waiting` state, so an
agent inside one always has somewhere to park. An agent with no loop has nowhere. Letting it park anyway would
produce work that waits forever — and on every screen, work waiting forever looks exactly like work waiting patiently
for a person.

⚑ So the capability follows from the structure rather than from a flag. There is nothing to switch on, nothing to
forget, and no way to end up with an agent that can ask but cannot be answered.

### Nothing is suspended, and nothing needs to be   {#mechanism}
This is the part worth understanding, because it explains every other rule on this page.

An agent's turn loop is ordinary code — call the model, run the tools it asks for, repeat. It cannot be frozen across
the hours a person takes to reply, and no amount of machinery would change that.

It does not have to be. **An agent's state is its conversation**, and that is already durable:

1. The agent calls the ask verb. That turn is written down **with no result** — an unanswered question is exactly
   what a parked run looks like.
2. Everything stops. The task reads `Waiting`; the workflow holds the wait as a `Slot` with an assignee, an
   opened-at and an SLA clock.
3. The person answers. Their words are written as **that call's result**.
4. A fresh run replays the conversation — the original instruction, the agent's own question, and now its answer —
   and the model carries on as though it had called a very slow tool.

Nothing was suspended, so nothing has to be woken. This is also why an agent that keeps no conversation cannot do
any of it, and why that is refused **at the ask** rather than discovered at the answer.

### An agent that retains nothing is refused when it asks   {#memory-required}
Asking requires `Memory = Persistent` on the agent. Its history *is* its continuation, so an agent with none has
nothing to come back to.

The refusal happens the moment it tries to ask, not later when somebody answers — because a run that parks and can
never be resumed is a dead end, and a dead end is indistinguishable from patience on every screen you would look at.
The message names the one line that fixes it.

### Parking again is ordinary   {#park-again}
`Answer` returns an `AgentReply`, and it can come back parked. An answer that raises one more question is what
conversations do.

Branch on it, or you will move to a terminal state over work that is still waiting on somebody:

```osy syntax
on Answered(string text, Slot slot) {
  var reply = Auditor.Answer(this.Item, text);
  if (reply.Parked) { goto Waiting; }     // it asked one more thing
  goto Completed;
}
```

### What the platform owns, and what you own   {#split}

| | |
|---|---|
| **The platform** | that the run can park at all; that the wait is a `Slot` with an assignee, an opened-at and a clock; that the answer arrives as the question's result; that the same task and the same conversation carry on |
| **You** | who is asked · how they hear about it · what the question looks like on screen · how long they get · what happens if they never reply |

The second column is not an oversight. Any opinion the platform held about who to notify would be wrong in every app
that disagreed — so the `Waiting` state's `enter` body is yours, and so is whatever row you write to put the question
in front of somebody.

### Answering goes through the slot   {#answering}
Deposit the answer into the workflow slot rather than calling `Answer` from a page. The slot is what decides whether
*this* caller may answer, and what records that they did and how long they took — which is what makes *"it waited 15
hours for Sam"* a question with an answer instead of a shrug.

```osy syntax
void AnswerAuditor(Question question, string text) {
  ReviewClaim.For(question.Task).Filer.Answered(text);
}
```

The workflow's own `on Answered` arm is what calls `Answer`. Depositing is server-side because the authority check
is: a browser cannot decide who is allowed to answer.

### What the person is shown   {#question}
`reply.Question` is what the agent asked, in its own words. It is handed back rather than dropped because it is the
whole content of the work item — an inbox that knows an agent is waiting but not what it wants is not an inbox.

Write it somewhere your own screens can read. What that row looks like is yours; the demo keeps a `Question` row
against the claim, and renders it at the top of the page the person is already on.

## Examples       {#examples}

The smallest app in which an agent can ask a person. Everything here exists to make the two marked lines reachable —
the agent's work runs inside a loop, so it may ask; and the answer goes in through the slot:

```osy title="agent-asks-a-person" test app=agent-hitl
using Osysharp.Agents;

[Principal] entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Claim {
  [Required, MaxLength(200)] string Title;
  security { allow read, create, update when IsAuthenticated; }
}

/// What the agent asked. The platform parks the run; this row is what a person actually sees.
entity Question {
  [Required] ReviewTask Task;
  [Required, MaxLength(2000)] string Text;
  [MaxLength(2000)] string? Answer;
  security { allow read, create, update when IsAuthenticated; }
}

entity ReviewTask : AgentTask {
  security { allow read when IsAuthenticated; }
}

agent Auditor {
  Purpose   = "Review a claim.";
  Prompt    = "You review expense claims.";
  Memory    = Persistent;                        // required: an agent that retains nothing cannot come back
  Principal = new User { Email = "auditor@example.com" };
  Loop      = ReviewClaim;                       // …and this is what lets it ask at all
}

workflow ReviewClaim {
  Tracks    = ReviewTask.Status;
  Autostart = true;
  Initial   = Running;

  event Answered(string text);

  state Running {
    enter {
      var turns = new List<Turn>();
      turns.Add(Turn.User("Review this claim."));

      var reply = Auditor.Ask(turns);
      if (reply.Parked) {                        // ← IT ASKED SOMEBODY
        Ask(this.Item, reply.Question ?? "");
        goto Waiting;
      }
      goto Completed;
    }
  }

  state Waiting {
    subscribe Answered(string text) as Filer {
      Candidates = u => u.Email != "";
      Finished { Within = TimeSpan.FromDays(2); Unfinished { goto Failed; } }
    }

    on Answered(string text, Slot slot) {
      var reply = Auditor.Answer(this.Item, text);   // ← AND THIS IS THE ANSWER REACHING IT
      Close(this.Item, text);
      if (reply.Parked) { goto Waiting; }
      goto Completed;
    }
  }

  terminal success Completed { }
  terminal error   Failed { Message = "the review could not be completed"; }
}

void Ask(ReviewTask task, string text) {
  var q = new Question { Task = task, Text = text };
  UnitOfWork.Commit();
}

void Close(ReviewTask task, string text) {
  var open = Question.Where(q => q.Task == task && q.Answer == null).FirstOrDefault();
  if (open != null) { open.Answer = text; UnitOfWork.Commit(); }
}

/// What your page calls when the person replies.
void AnswerAuditor(Guid questionId, string text) {
  var question = Question.Where(q => q.Id == questionId).FirstOrDefault();
  ReviewClaim.For(question.Task).Filer.Answered(text);
}
```

The same example with the surrounding app — a real claim, a filer, and the trigger that starts the work — is on
[[agent-loop#examples|the loop page]].

## See also       {#see-also}
- [the agent loop (app.Agent, Loop)](https://osysharp.com/reference/agent/loop/) — the workflow the work runs inside, and what makes asking possible
- [running an agent from your code](https://osysharp.com/reference/agent/ask/) — running a declared agent, and the `AgentReply` both verbs answer
- [Workflow.Work&lt;T&gt; (everything outstanding) and its SLA numbers](https://osysharp.com/reference/workflow/work/) — slots, assignees and SLA clocks: what a wait is made of
- [the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/) — the task that stays open while the run is parked


---

<!-- https://osysharp.com/reference/agent/models/ -->

# app.Models — the models an app admits, each with a name

> `app.Models` declares more than one model your app may use, each under a name you choose. The name is what your code and your users refer to — `Llm.Fast` — so a call site never quotes a provider's model string, and pointing a name at a different model is a one-line change.

<!-- id: agent-models · area: agent · stability: preview · html: https://osysharp.com/reference/agent/models/ -->

## Summary        {#summary}
[default LLM model (app.DefaultModel)](https://osysharp.com/reference/agent/default-model/) declares one model. `app.Models` declares several, each under a name — so your app can offer
a choice, or use a cheap model for one job and a strong one for another.

## Signature      {#signature}
```osy syntax
app.Models = [
  new LlmConfig("<name>") { Provider = LlmProvider.X, Model = "…", ApiKey = Secret.X, BaseUrl = "…" },
  …
];

app.DefaultModel = Llm.<name>;
```

## Description    {#description}
Each entry is the same `LlmConfig` you would write for a single default, plus a **name of your choosing**.

**The name is a real name, not a string.** You refer to it as `Llm.Fast`, and a name nothing declares is a compile
error that lists what you did declare. So a typo fails the build rather than quietly matching no model, and renaming
an entry breaks every place that used it — which is what you want, because those are the places that need updating.

**Name the models for what they are to your app, not for what the vendor calls them.** `Llm.Fast` and `Llm.Smart`
stay true when you point them at newer models next month; `Llm.Haiku` becomes a lie. The name is also what your users
see if you build a picker.

**`app.DefaultModel` says which one serves a call that does not ask for a particular model:**

```osy syntax
app.DefaultModel = Llm.Fast;
```

Pointing the default at a name rather than repeating the settings means there is one description of each model, so
the default can never disagree with the entry it names. You can still write `app.DefaultModel = new LlmConfig { … }`
directly — that is the right form for an app with only one model, and it keeps working unchanged.

**Your app can only use models it declares.** Naming one at a call site can narrow the choice to any declared model,
but never reach one you did not declare — that is refused, with a reason, rather than quietly served by something
else. `app.Models` is how a model becomes available to be chosen.

**Removing an entry removes the model.** The list is reconciled against your source on every compile, so deleting a
line withdraws that model from the app rather than leaving it behind.

`DefaultModel` is a reserved name — it is how the default itself is stored — so an entry cannot be called that.

## Examples       {#examples}

Two models, one of them the default:

```osy title="two models from one provider, one of them the default" test app=models-two
app.Secrets = [ new Secret("Anthropic") ];

app.Models = [
  new LlmConfig("Fast")  { Provider = LlmProvider.Anthropic, Model = "claude-haiku-4-5", ApiKey = Secret.Anthropic },
  new LlmConfig("Smart") { Provider = LlmProvider.Anthropic, Model = "claude-opus-5",    ApiKey = Secret.Anthropic },
];

app.DefaultModel = Llm.Fast;

entity Note { [MaxLength(140)] string Title; }
```

Models from two different providers, which is the case a single default cannot express at all:

```osy title="two different providers — what a single default cannot express" test app=models-two-providers
app.Secrets = [ new Secret("Anthropic"), new Secret("OpenAI") ];

app.Models = [
  new LlmConfig("Writer")  { Provider = LlmProvider.Anthropic, Model = "claude-opus-5", ApiKey = Secret.Anthropic },
  new LlmConfig("Checker") { Provider = LlmProvider.OpenAI,    Model = "gpt-5",         ApiKey = Secret.OpenAI },
];

app.DefaultModel = Llm.Writer;

entity Draft { [MaxLength(140)] string Title; }
```

## See also       {#see-also}
- [default LLM model (app.DefaultModel)](https://osysharp.com/reference/agent/default-model/) — declaring a single model, for an app that needs only one
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — where the `Secret.X` an entry names comes from
- [LlmClient.Stream — a model's answer as it is written](https://osysharp.com/reference/function/llm-stream/) — calling a model


---

<!-- https://osysharp.com/reference/agent/default-model/ -->

# default LLM model (app.DefaultModel)

> Declares the app's default LLM — the provider, model, API-key secret, and optional endpoint — as a single app-level config. `Provider` is one of `LlmProvider.Anthropic | OpenAI | Gemini | OpenAICompatible`; the API key is a `Secret.X` handle declared in `app.Secrets`. A singleton: declaring it twice is a compile error, and removing it clears the default.

<!-- id: agent-default-model · area: agent · stability: preview · html: https://osysharp.com/reference/agent/default-model/ -->

## Summary        {#summary}
`app.DefaultModel` declares the application's default large-language model as one app-level config block. You name a
**provider**, a **model** string, the **API key** (a `Secret.X` handle), and an optional custom **BaseUrl**. It is a
per-app **singleton** — declared once — and it self-reconciles against source: removing it clears the default.

```osy syntax
app.Secrets = [ new Secret("Anthropic") ];
app.DefaultModel = new LlmConfig {
  Provider = LlmProvider.Anthropic,
  Model    = "claude-opus-4-8",
  ApiKey   = Secret.Anthropic,
};
```

## Signature      {#signature}
```osy
app.Secrets = [ new Secret("Anthropic") ];   // the handle the config below reads

app.DefaultModel = new LlmConfig {
  Provider = LlmProvider.Anthropic,   // Anthropic | OpenAI | Gemini | OpenAICompatible
  Model    = "claude-opus-4-8",       // the provider's model id
  ApiKey   = Secret.Anthropic,        // a handle declared in app.Secrets
  BaseUrl  = "https://api.anthropic.com",  // optional — required for OpenAICompatible
};
```

A single app-level assignment. It is a singleton — declaring it twice is a compile error.

## Description    {#description}
`LlmConfig` has four members:

- **`Provider`** — a qualified `LlmProvider` enum member. `Anthropic`, `OpenAI`, and `Gemini` are first-party
  providers reached at their default endpoints. `OpenAICompatible` targets any OpenAI-wire-compatible server (a
  self-hosted model, a proxy, or an OpenRouter-style gateway) and **requires** a `BaseUrl`.
- **`Model`** — the provider's model identifier (e.g. `claude-opus-4-8`, `gpt-4o`, `gemini-2.5-flash`).
- **`ApiKey`** — a `Secret.X` handle. The secret must be declared in [`app.Secrets`](https://osysharp.com/reference/config/secrets/); the key is
  resolved at runtime from the secret store and never inlined in source.
- **`BaseUrl`** *(optional)* — a custom endpoint. Optional for the first-party providers (they default to their own
  endpoints); required for `OpenAICompatible`.

Because it self-reconciles against source, the config behaves like a switch: **declaring it** upserts the singleton,
**removing it** sweeps the row and clears the app default (no prune step needed). Every LLM call the platform makes on
your behalf is metered against your budget regardless of which provider you choose.

## Examples       {#examples}
Point the app at an OpenAI-compatible endpoint you host yourself — `OpenAICompatible` plus a `BaseUrl`:

```osy title="default-model-openai-compatible" test app=default-model-openai-compatible
app.Secrets = [ new Secret("LocalLlm") ];
app.DefaultModel = new LlmConfig {
  Provider = LlmProvider.OpenAICompatible,
  Model    = "llama-3.1-70b",
  ApiKey   = Secret.LocalLlm,
  BaseUrl  = "https://llm.internal.example.com/v1",
};
```

## See also       {#see-also}
- [[Searchable]](https://osysharp.com/reference/memory/searchable/) — `[Searchable]`, the field-search surface; semantic ranking uses the app embedding configured the same way


---

<!-- https://osysharp.com/reference/agent/ask/ -->

# running an agent from your code

> Call an agent you declared the way you would call anything else you declared — by name. You hand it the turns you want it to see: the question, the history worth replaying, the context you assembled. Its own declaration supplies its instructions, its tools and its model, so the call site stays about what you are asking. The answer comes back as a string, and the run is recorded as a task with its cost and its outcome.

<!-- id: agent-ask · area: agent · stability: stable · html: https://osysharp.com/reference/agent/ask/ -->

## Summary        {#summary}

An agent is something you declare, so it is something you can call:

```osy syntax
var reply = Triager.Ask(turns);
if (reply.Parked) { /* it asked a person */ } else { /* reply.Text */ }
```

The receiver is the agent's own name. That is not decoration — it means the compiler checks the agent exists, the
same way it checks any other name you write, instead of finding out at run time that a string was misspelled.

## Signature      {#signature}

```osy syntax
List<Turn> turns = new List<Turn>();
turns.Add(Turn.Context("Acme Ltd, plan Enterprise, customer since 2019"));
turns.Add(Turn.User("Is the Lisbon dinner within policy?"));

AgentReply reply = Triager.Ask(turns);
string answer = reply.Text;
```

| you write | it means |
|---|---|
| `Turn.User(text)` | somebody is asking |
| `Turn.Assistant(text)` | the agent said this earlier — replayed history |
| `Turn.Context(text)` | facts you supplied. Not dialogue |
| `<agent>.Ask(turns)` | run that agent over those turns; answers an `AgentReply` |
| `<agent>.StartTask<T>(trigger)` | hand the work to the agent's [[agent-loop\|loop]] instead of running it here |

## Description    {#description}

**You assemble the turns, and that is deliberate.** Which history to replay and which facts to include are product
decisions — a support screen sends the customer and their recent orders, a nightly audit sends one line. A platform
that guessed for you would guess wrong in both directions: too much costs money on every call, too little produces a
confident answer to a question the agent could not actually see.

⚑ **Say which kind each turn is.** `Turn.Context` is the one people skip, and it is the one that matters most: an
agent that cannot tell the facts it was handed from the question it was asked will answer the facts. Reading back a
customer record is a very convincing wrong answer.

**The agent brings itself.** Its instructions, its tools, its model and its memory are on its declaration, so they
are not repeated at the call site and cannot drift between two places that call the same agent.

**Everything the run did is recorded.** The call returns the answer; the rest is a task — what it cost, what it
called, how long it waited, and what it produced. See [the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/) and [what an agent hands back (AgentDeliverable)](https://osysharp.com/reference/agent/deliverables/). That is why the
return is a plain string rather than a result object: the interesting detail is queryable, and putting it in the
return type would make every call site hold something it did not ask for.

**A repeated run does not repeat the call.** If the work is resumed after an interruption, the answer already given
is the answer used — the agent is not asked twice. That matters for cost, and because the same question need not
produce the same answer twice.

⚠ **Asking a person suspends the work.** If the agent decides it needs a human, the run stops and waits rather than
inventing an answer, and `Ask` says so instead of returning an empty string. The waiting run is recorded and can be
picked up once somebody answers.

## Examples       {#examples}

Checking something with the context the app already has:

```osy title="ask-an-agent" test app=agent-ask
using Osysharp.Agents;

[Principal]
entity User {
  [Required, MaxLength(100)] string DisplayName;
  security { allow read when IsAuthenticated; allow create when IsAuthenticated || IsAnonymous; }
}

agent Triager {
  Purpose   = "Triage expense reports.";
  Prompt    = "You triage expenses against the travel policy.";
  Principal = new User { DisplayName = "Triager" };
}

string Check(string question, string customer) {
  var turns = new List<Turn>();
  turns.Add(Turn.Context(customer));
  turns.Add(Turn.User(question));
  return Triager.Ask(turns).Text;
}
```

### The agent may stop to ask a PERSON   {#parked}
An agent that needs something only a human can supply does not fail and does not guess — it stops and asks. `Ask`
answers that as an outcome you branch on rather than an error thrown out from under you:

```osy syntax
var reply = Triager.Ask(turns);

if (reply.Parked) {
  // The run is recorded and resumable. `reply.Question` is what it wants to know —
  // put it in front of a person, and the run continues when they answer.
  return reply.Question;
}

return reply.Text;
```

| member | |
|---|---|
| `Text` | what the agent said. Empty when parked — never null, so emptiness never has to mean two things |
| `Parked` | it stopped to ask a person, and is waiting |
| `Question` | what it asked, when parked |

⚠ **Branch on `Parked`, not on an empty `Text`.** An agent may legitimately reply with nothing, so the two are
deliberately different answers.

### Handing the work to the loop instead   {#start-task}
`Ask` runs the agent *now* and waits. When the work is big enough to deserve durability, a human step or a retry,
hand it to the agent's [loop](https://osysharp.com/reference/agent/loop/) instead:

```osy title="handing the work to the loop instead of waiting" syntax
Guid task = Triager.StartTask("Review the Lisbon contract.");
```

That opens the agent's task and returns; the declared loop starts on it and runs the agent inside a workflow that
can park, escalate and be resumed. The usual shape is to decide which you want first — a cheap classifying `Ask`,
then `StartTask` only for the work that warrants it.

Name a task type to choose *which* loop runs, when your app declares more than one:

```osy title="naming which loop runs, when there is more than one" syntax
Guid task = Triager.StartTask<PersonalTask>("Review the Lisbon contract.");
```

The loop that tracks that type is the one that starts. The type must be one of yours deriving from `AgentTask`.

## See also       {#see-also}
- [the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/) — what the run cost, what caused it, and how long it took.
- [what an agent hands back (AgentDeliverable)](https://osysharp.com/reference/agent/deliverables/) — what the agent hands back beyond a sentence.
- [agent conversation memory (using Osysharp.Agents)](https://osysharp.com/reference/agent/conversation-memory/) — turns the platform records for you.
- [the agent loop (app.Agent, Loop)](https://osysharp.com/reference/agent/loop/) — the workflow a started task runs inside.


---

<!-- https://osysharp.com/reference/agent/task-stop/ -->

# stopping work (task.Stop)

> End a task and everything beneath it, including the model call it is in the middle of. Answers whether a live run was actually interrupted, as opposed to the stop being recorded for work that had not started yet. Who may stop work is who may update the task.

<!-- id: agent-task-stop · area: agent · stability: preview · html: https://osysharp.com/reference/agent/task-stop/ -->

## Summary        {#summary}

```osy syntax
var interrupted = task.Stop();
```

Marks the task and everything under it as `Cancelled`, and **trips the model call in flight** so the agent stops
now rather than after the answer it was already paying for.

## Signature      {#signature}

```osy syntax
task.Stop()   // → bool — true when a LIVE run was interrupted
```

The return value distinguishes two real outcomes: **stopped** (a run was executing and has been cut off) and **will
stop** (nothing was running yet, and the request is remembered so the next run ends immediately). A stop button that
conflates them tells the user "done" when the work is still going.

## Description    {#description}

### It stops the work, not just the record   {#actually-stops}

A task can be sitting inside a provider call that takes tens of seconds. `Stop()` cancels it: the call is abandoned,
the run unwinds, and nothing further is charged for that turn. The task's row is marked in the same operation, so a
[watcher](https://osysharp.com/reference/agent/task-watch/) ends and every list stops showing the task as running.

⚑ Both halves matter. Marking the row alone leaves the agent talking to the model; cancelling alone leaves every
screen claiming the work is still in progress.

### Does it stop child tasks too?   {#subtree}

A task driven by a [loop](https://osysharp.com/reference/agent/loop/) is the parent; the run doing the work is a child. `Stop()` therefore ends the
task **and everything beneath it** — stopping only the task you were handed would leave the agent running
underneath it.

⚠ Tasks that have already finished are left alone. A child that completed a second before the stop *did* complete,
and its record of that is worth more than a uniform status across the tree.

### `Cancelled` is not `Failed`   {#cancelled}

A stopped task ends as `Cancelled`. That is deliberately distinct from `Failed`: a failure is the work going wrong
and worth investigating, a stop is somebody deciding it should not continue. If they shared a state, every operator
abort would look like a defect on the one screen you scan for defects.

### Who can stop work   {#security}

**Stopping is an update.** Declare who may update the task and you have declared who may stop it:

```osy syntax
entity ReviewTask : AgentTask {
  security {
    allow read when IsAuthenticated;    // everyone signed in can WATCH
    allow update when IsApprover;       // …only an approver can STOP
  }
}
```

⚠ **Read and update are separate on purpose.** Watching work and ending it are different privileges, and an app that
lets everyone see a task usually does not mean everyone may kill it. A caller without `update` is refused with a
message naming the rule they need.

### What a watcher sees   {#watchers}

A stop appends a final step, so a [`Watch()`](https://osysharp.com/reference/agent/task-watch/) feed ends with a line saying what happened rather
than simply going quiet. A stream that stops producing is otherwise indistinguishable from one that finished
normally.

## Examples       {#examples}

```osy title="stopping-a-review" test app=agent-task-stop
using Osysharp.Agents;

[Principal] entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read when IsAuthenticated; }
}

entity ReviewTask : AgentTask {
  security {
    allow read when IsAuthenticated;
    allow update when IsAuthenticated;
  }
}

/// Ends the work and says whether anything was actually interrupted.
string StopReview(Guid taskId) {
  var task = ReviewTask.Where(t => t.Id == taskId).FirstOrDefault();
  if (task == null) { return "no such task"; }

  return task.Stop() ? "stopped" : "nothing was running — it will not start";
}
```

## See also       {#see-also}
- [watching a task run (task.Watch)](https://osysharp.com/reference/agent/task-watch/) — following work while it runs, and seeing it end
- [the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/) — the task itself: what caused it, when it ran, how it finished
- [the agent loop (app.Agent, Loop)](https://osysharp.com/reference/agent/loop/) — why a task and the run beneath it are two rows


---

<!-- https://osysharp.com/reference/agent/loop/ -->

# the agent loop (app.Agent, Loop)

> The workflow an agent's work runs INSIDE. A step of it means "run the agent until it finishes or asks for help" — the workflow supplies durability, the human slot, escalation, retries and the trace, while the agent supplies the turns. Declared once for the app and overridable per agent. Naming a workflow that does not exist is a compile error, because the failure it would otherwise cause is an absence nothing reports.

<!-- id: agent-loop · area: agent · stability: preview · html: https://osysharp.com/reference/agent/loop/ -->

## Summary        {#summary}
An agent that answers a question in one turn needs nothing around it. An agent that does **work** — reads several
things, calls tools, pauses for a person, resumes hours later — needs everything a workflow already provides. So the
work runs inside one, and you say which:

```osy syntax
app.Agent = new AgentConfig { Loop = ProcessTask };    // the app default
```

The workflow **wraps** the turn loop; it does not become it. One step of it means *"run the agent until it finishes
or asks for help"*.

## Signature      {#signature}
```osy syntax
app.Agent = new AgentConfig {
  Loop = <workflow>          // the default loop for every agent in this app
};

agent Escalator {
  Loop = <workflow>;         // …overridden for this one
}
```

Both take a bare workflow NAME. Omit `Loop` on an agent and it takes the app default; omit `app.Agent` too and the
agent's work runs with no workflow around it — which is the right answer for an app whose agents only ever answer in
the turn.

## Description    {#description}

### The workflow supplies the durability, the agent supplies the turns   {#boundary}
This split is the whole design. The workflow owns:

- **durability** — the run survives a restart, a deploy, and hours of waiting;
- **the human slot** — when the agent asks a person something, the wait is a modelled fact with an assignee, an
  opened-at and an SLA clock, not a paused process;
- **escalation and retries** — declared, not hand-written;
- **the trace** — what happened, in order, queryable afterwards.

The agent owns the turns: call the model, dispatch tools, repeat. That loop stays in one place. If a workflow could
express it too there would be two implementations of the same thing, and they would drift.

### The loop's subject is the TASK   {#subject}
A loop workflow runs **over the agent's task** — the same row the work log is built from — and it declares so:

```osy syntax
workflow ProcessTask {
  Tracks  = AgentTask.Status;
  Initial = Running;
  state Running { }
  state Waiting { }              // parked on a person
  terminal success Completed { }
  terminal error   Failed { }
}
```

Two things follow, and both are the reason for the choice:

- **"What has this agent done, or is waiting for" stays one column to list on.** The task's `Status` *is* the
  workflow's state. A loop with a state of its own would make that two questions, and a list view would have to know
  which of them to ask.
- **The state transition log is the task's.** Every move the loop makes is recorded against the piece of work it is
  about, so the history is where you would look for it.

⚠ **While a loop drives a task, the workflow OWNS `Status` and nothing else writes it.** The agent's turn loop
finishing is not the task finishing — a run parked on a person is still parked, and the task goes on reading
`Waiting` until the workflow moves it. What the platform still records directly is what that run *produced*: its
outcome and when it stopped.

### What a loop must at least have   {#minimum}
The platform does not supply the loop. It checks that yours **can do the two things a loop is for**, and refuses at
compile time when it cannot:

| the rule | why it is a rule |
|---|---|
| `Tracks = AgentTask.Status` | a loop runs over the agent's own task; tracking something else leaves the task's status written by nothing |
| a state for **every** member of `AgentTaskStatus` | a missing member is a state the task can reach and the workflow has nowhere to put |

`Waiting` is the one that matters. Without it an agent that asks a person has nowhere to park — and that failure is
an **absence**: the work runs, nobody is asked, and there is no exception to catch and no log line to find. Said at
compile time it is one sentence naming what to add.

Everything else is yours. Who gets notified when the run parks, what the SLA is, how it escalates, how many times it
retries — the platform has no opinion, because any opinion it had would be wrong in the apps that disagreed. That is
what the `Waiting` state's `enter` body is for.

⚑ **This is also why samples are safe to copy.** Start from one, change it into what your app needs — and if the
protocol ever demands more, your copy stops **building** with a sentence naming what to add, rather than quietly
going on doing less than it used to.

### Not every task gets one   {#which-tasks}
- **Work a workflow already drives** needs no second loop — the task points at the run that is already going, and
  starting a second would put two state machines on one piece of work.
- **Work started by a chat message, an event or a schedule** runs in the declared loop, *when the work warrants it*.
  Opening the task starts the run, bound to that task — so [`<agent>.StartTask(trigger)`](https://osysharp.com/reference/agent/ask/) is how a
  trigger hands work to the loop, and the loop is what runs the agent.
- **An agent spawned by another agent** rides its parent's loop; it starts nothing of its own.

⚠ **Do not spin durable machinery for a one-turn answer.** *"What did we spend on travel in Q3"* should cost a model
call, not a workflow run. Deciding which is which is your app's job — classify the incoming message first, answer the
questions, and start the loop only for the work:

```osy syntax
var verdict = Triager.Ask(turns).Text;          // cheap: decide whether this is work
if (verdict == "work") {
  juniorLegal.StartTask("…");                   // durable: the loop takes it from here
}
```

### Which loop runs is decided by the task's TYPE   {#by-type}
A loop tracks a task type, and it starts for rows of that type and no other. So an app with more than one kind of
agent work declares a task type per kind and a loop per type:

```osy syntax
entity PersonalTask : AgentTask { security { allow read when IsAuthenticated; } }

workflow PersonalTaskProcessor {
  Tracks    = PersonalTask.Status;
  Autostart = this.Item.Type != AgentTaskType.Workflow && this.Item.Type != AgentTaskType.Spawn;
  Initial   = Running;
  state Running { }
  state Waiting { }
  terminal success Completed { }
  terminal error   Failed { }
}
```

`Autostart` is a plain condition over the task, written directly — the row is in scope as `this.Item`, so there is
no lambda to introduce. It is also where *"not every task gets one"* is stated in your own words rather than assumed:
a task a workflow already drives, or one an agent spawned, is excluded here.

### A name that resolves to nothing is refused   {#refusal}
```text
`app.Agent.Loop` names `ProcessTsak`, which is not a declared workflow — add `workflow ProcessTsak { … }`, whose
step runs the agent until it finishes or asks for help. Left unresolved the work would run with no workflow around
it: no durability, no human slot, no trace, and nothing to say so.
```

This is a compile error rather than a runtime one because **the runtime failure would be an absence**. Nothing throws;
the work simply runs bare, and an app that never asked for a loop looks exactly the same. There is no log line to
find, because nothing went wrong — something did not happen.

The same check covers the per-agent override, which is the same mistake in the other place it can be written.

### The override is on the agent, and the default is resolved at run time   {#override}
An agent that declares no `Loop` stores none — it is **not** stamped with the app default at compile time. That is
deliberate: changing `app.Agent` would otherwise leave every previously-compiled agent pointing at the old workflow.
An empty `Loop` means *"ask the app"*, and it is answered each time the work starts.

An `agent` declaration seeds a template, and the template carries the `Loop` too — so an instance minted from it
later inherits the declaration rather than falling back to the default.

## Examples       {#examples}

**A real loop, and the reason loops exist.** The agent reviews a claim; when it cannot judge one without the person
who filed it, it asks — the run parks in `Waiting`, and their answer hours later resumes the very same run:

```osy title="agent-loop-hitl" test app=agent-loop-hitl
using Osysharp.Agents;

[Principal] entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Claim {
  [Required, MaxLength(200)] string Title;
  [Required] User Filer;
  security { allow read, create, update when IsAuthenticated; }
}

/// What the agent asked, and what it was told. The platform parks the run; what the question LOOKS like to the
/// person answering it is yours.
entity Question {
  [Required] Claim Claim;
  [Required] ReviewTask Task;
  [Required, MaxLength(2000)] string Text;
  [MaxLength(2000)] string? Answer;
  security { allow read, create, update when IsAuthenticated; }
}

entity ReviewTask : AgentTask {
  security { allow read when IsAuthenticated; }
}

agent Auditor {
  Purpose   = "Review a claim and recommend approve or send-back.";
  Prompt    = "You review expense claims.";
  Memory    = Persistent;                       // an agent that retains nothing cannot come back from a park
  Principal = new User { Email = "auditor@example.com" };
  Loop      = ReviewClaim;
}

workflow ReviewClaim {
  Tracks    = ReviewTask.Status;
  Autostart = true;
  Initial   = Running;

  event Answered(string text);

  state Running {
    enter {
      var claim = Claim.Where(c => c.Id == this.Item.EntityId).FirstOrDefault();
      if (claim == null) { goto Failed; }

      var turns = new List<Turn>();
      turns.Add(Turn.Context("Claim: " + claim.Title));
      turns.Add(Turn.User("Review this claim."));

      var reply = Auditor.Ask(turns);
      if (reply.Parked) {                        // it stopped to ask a person
        OpenQuestion(claim, this.Item, reply.Question ?? "");
        goto Waiting;
      }
      goto Completed;
    }
  }

  state Waiting {
    enter { Log.Information("waiting on the filer of {Claim}", this.Item.EntityId); }   // notifying is yours

    subscribe Answered(string text) as Filer {
      Assignee = Claim.Where(c => c.Id == this.Item.EntityId).FirstOrDefault().Filer;
      Finished { Within = TimeSpan.FromDays(2); Unfinished { goto Failed; } }
    }

    on Answered(string text, Slot slot) {
      var reply = Auditor.Answer(this.Item, text);   // their words reach the agent as its question's answer
      CloseQuestion(this.Item, text);
      if (reply.Parked) { goto Waiting; }            // it asked one more thing — ordinary, not an edge case
      goto Completed;
    }
  }

  terminal success Completed { }
  terminal error   Failed { Message = "the review could not be completed"; }
}

void OpenQuestion(Claim claim, ReviewTask task, string text) {
  var q = new Question { Claim = claim, Task = task, Text = text };
  UnitOfWork.Commit();
}

void CloseQuestion(ReviewTask task, string text) {
  var open = Question.Where(q => q.Task == task && q.Answer == null).FirstOrDefault();
  if (open != null) { open.Answer = text; UnitOfWork.Commit(); }
}

/// The trigger. `about:` is what lets the task say which claim it is for.
Guid StartReview(Claim claim) {
  return Auditor.StartTask<ReviewTask>("claim submitted: " + claim.Title, about: claim);
}

/// The person's answer, on its way to the parked run.
void AnswerAuditor(Question question, string text) {
  ReviewClaim.For(question.Task).Filer.Answered(text);
}
```

Read the three moments in order. `StartReview` opens the task and returns — the submit gesture does not wait for a
model. `Running`'s `enter` body runs the agent and **branches on the park** rather than being thrown out of. And
`on Answered` hands the person's words back with [`Answer`](https://osysharp.com/reference/agent/hitl/), which is the hop the whole mechanism is
for: nothing was suspended, so nothing has to be woken.

⚠ **Everything in `Waiting` is yours.** Who is asked, how they hear about it, how long they get, what happens when
they do not reply. The platform's half was making the run parkable at all.

A minimal loop, when you only need the wiring — every state present, no body yet:

```osy title="agent-loop-default" test app=agent-loop
using Osysharp.Agents;

[Principal] entity User {
  [Required, MaxLength(100)] string DisplayName;
  security { allow read when IsAuthenticated; allow create when IsAuthenticated || IsAnonymous; }
}

workflow ProcessTask {
  Tracks  = AgentTask.Status;
  Initial = Running;
  state Running { }
  state Waiting { }
  terminal success Completed { }
  terminal error   Failed { }
}

app.Agent = new AgentConfig { Loop = ProcessTask };

agent Triager {
  Purpose   = "Triage incoming tickets.";
  Prompt    = "You triage tickets.";
  Principal = new User { DisplayName = "Triager" };
}
```

A chat message that turns into work now opens a task, starts a run of `ProcessTask` bound to it, and the task's
`Status` reads whatever state that run is in.

⚠ **This one demonstrates the wiring and nothing else** — a reader meeting the feature here would learn that a loop
is a two-state workflow and come away with no idea why they would want one. The example above it is the feature.

## See also       {#see-also}
- [an agent asking a person (the human slot)](https://osysharp.com/reference/agent/hitl/) — the ask-a-human hop in full: what parks a run, and what resumes it
- [running an agent from your code](https://osysharp.com/reference/agent/ask/) — running a declared agent from your own code
- [Workflow.Work&lt;T&gt; (everything outstanding) and its SLA numbers](https://osysharp.com/reference/workflow/work/) — the human slot the loop gives an agent that needs to ask
- [the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/) — the task an agent's work opens, and what it records


---

<!-- https://osysharp.com/reference/agent/task-log/ -->

# the agent task log (AgentTask)

> Every piece of work an agent does is recorded as an `AgentTask` — which agent, what set it going, when it started and finished, and what came of it. Tasks nest: work an agent starts from inside another piece of work becomes a sub-task, so "what did this whole job involve" is one query however many agents it took. The rows are written by the platform and can never be created, edited or deleted by app code; your app decides who may READ them.

<!-- id: agent-task-log · area: agent · stability: stable · html: https://osysharp.com/reference/agent/task-log/ -->

## Summary        {#summary}

You can see what an agent has **said** — that is the conversation. The task log is what it **did**.

Each piece of work an agent performs is one `AgentTask` row: whose work it was, what caused it, when it ran, and how
it ended. Because agent work sets off more agent work — an agent hands part of a job to another agent, or files a
record that starts a process which itself needs an agent — tasks form a **tree**. A task knows its parent and carries
its full ancestry, so you can ask about one step or about everything that step led to, at any depth.

`AgentTask` arrives with `using Osysharp.Agents;`, alongside the rest of the agent surface.

## Signature      {#signature}

```osy syntax
using Osysharp.Agents;

partial entity AgentTask {
  security { allow read when IsAuthenticated; }
}
```

The fields you will read:

| field | what it holds |
|---|---|
| `Agent` | whose work this was |
| `Type` | what set it going — `Workflow`, `Event`, `Schedule`, `Chat`, or `Spawn` (another agent's task) |
| `Status` | `Running`, `Completed` or `Failed` |
| `Title` · `Trigger` | one line for a list; the full reason it ran |
| `Parent` · `Children` · `Path` · `RootTask` | the tree — see below |
| `StartedAt` · `CompletedAt` · `Outcome` | when, and how it ended |
| `WorkflowRun` · `ChatSession` | the process or conversation it belongs to, when it belongs to one |
| `EntityTypeName` · `EntityId` | the record it is about, when it is about one |
| `Principal` · `OnBehalfOf` | who it ran as, and whose behalf it ran on |

Cost is **not** a field — see below.

## Description    {#description}

**The organising unit is the AGENT, not the process.** The question the log answers is *"what has this agent done"* —
a list of its recent work with its type, its timing and its outcome. A business process is one of the things that can
cause agent work; plenty of processes never involve an agent at all, and those create nothing here.

**Not every agent turn is work.** A question the agent answers there and then — *"what did we spend on travel in
Q3?"* — is a conversation, not a job, and it writes no task. A task exists when something set the agent to WORK: a
process step, an event, a schedule, a request that turned into a piece of work, or another agent handing part of a
job over. That is deliberate: a log that recorded every exchange would bury the work in chatter.

**Tasks nest, across mechanisms.** An agent's work can create a record that starts a process, whose step needs agent
work, whose agent hands part of the job to another agent. Each of those is a sub-task of the one before it, and the
chain is recorded the same way regardless of what made each hop. A **sub-task means the work went to a different
actor** — another agent, or a person — not that the same agent moved on to its next step.

**`Path` is what makes "and everything it led to" cheap.** Every task carries its ancestry as a path ending in its own
id, so one filter selects a task together with all its descendants, however deep. Reading `Children` gives you the
immediate sub-tasks; matching on `Path` gives you the whole subtree.

**Cost is not a field either, and that is the same decision.** Every model call the task caused is recorded with the
task's id and its path, so *"what did this cost"* is a sum over those calls, and *"what did this cost all in"* is a sum
over every call whose path starts with this task's. Both are exactly the sum of the calls they summarise — a stored
total can disagree with its own calls after a run that failed half-way, with nothing to reconcile it against.

The calls are `LlmCallRecord` rows, which your app can already query. `AgentTask` is the task each call belongs to,
`TaskPath` is that task's ancestry, and the same aggregate over `InputTokens`/`OutputTokens` gives tokens instead of
money.

⚑ **It counts what the agent's TOOLS spent, not just its own turns.** A tool that runs an extraction or a
classification is doing model work the task caused, and it lands under the same task without anyone passing anything
— because the attribution happens where every model call goes through, not where turns are counted.

**Waiting is not recorded here.** A task that is waiting for a person is waiting inside a process, which already
records who it was assigned to, when it opened, when they picked it up and whether it ran late. The task points at
that process rather than keeping a second copy of the same clock — two copies of one fact drift the day somebody
fixes one of them.

**The rows are written by the platform.** App code can never create, change or delete an `AgentTask`: a fabricated row
would attribute work to an agent that never did it, and a deleted one would hide work that happened. Attempting a
write is refused.

**Who may READ them is entirely yours.** The platform ships no opinion about that, which means a plain `using
Osysharp.Agents;` gives you a table nobody can read yet. Declare the rule you want, the same way you would for any
other entity — everyone signed in, only managers, only the person the work was done for. Building the queue screen,
the review list and the activity dashboard is your app's job too; the log is the material they are built from.

## Examples       {#examples}

An app that lets any signed-in person see the agent work log:

```osy title="agent-task-log-read-rule" test app=agent-task-log
using Osysharp.Agents;

[Principal] entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read when IsAuthenticated; }
}

partial entity AgentTask {
  security { allow read when IsAuthenticated; }
}
```

With that in place, "what has this agent been doing" is an ordinary query over an ordinary entity — and so is "show
me everything that came out of this one job", by matching descendants on their `Path`.

## See also       {#see-also}
- [what an agent hands back (AgentDeliverable)](https://osysharp.com/reference/agent/deliverables/) — what the work PRODUCED: the documents, files and records the agent hands back.
- [agent conversation memory (using Osysharp.Agents)](https://osysharp.com/reference/agent/conversation-memory/) — what the agent SAID, as against what it did.
- [running an agent from your code](https://osysharp.com/reference/agent/ask/) — running an agent from your own code.


---

<!-- https://osysharp.com/reference/agent/ai-switch/ -->

# turning AI off (the runtime switch)

> An operator can switch an application's AI off while it runs — no recompile, no redeploy. Text generation and embedding are two independent switches, because they do different things and send different data. A refused call fails with a 403 naming which capability was off; it never silently returns nothing.

<!-- id: agent-ai-switch · area: agent · stability: stable · html: https://osysharp.com/reference/agent/ai-switch/ -->

## Summary        {#summary}
Every AI call an application makes passes through one place, and an operator can close it. Two switches, not one:
**text generation** and **embedding** are turned off independently. Neither needs a recompile, a redeploy or a
restart — the change reaches every process within about fifteen seconds.

A call made while its capability is off does not quietly return nothing. It fails, with a message naming which
capability was switched off and noting that the other is switched separately.

## Signature      {#signature}
```text
app-detail  →  AI
   ▢ Text generation — models may write for this app
   ▢ Embedding — semantic search, and every indexed row is sent to the model host
```

There is no Osy# declaration for this. It is deliberately not something an application can write about itself —
see [Who can change it](#who).

## Description    {#description}

### Why two switches and not one   {#two}
They differ on both axes that matter to whoever is turning AI off.

**What they do.** Generation samples text that influences something a person reads or acts on. An embedding is a
single forward pass producing a fixed-size vector: it *ranks*, it does not decide or write prose. It is still
inference — calling it "not AI" would not survive a compliance reading — but it is not what a rule about AI in
decisions is aimed at.

**What they send.** This is the one that catches people out. A generation call sends what somebody typed. Embedding
sends **every indexed row** to whoever hosts the model — much the larger flow of application data leaving the
platform. So a policy about *generated output* covers only the first, while a policy about *where data may go*
covers both.

Because only you know which policy you are applying, the platform models the two and composes neither. If you want
one "AI off" control, turn both off.

> ⛔ **"AI is off" is false while either one runs.** Turning generation off and reporting "we disabled AI" is a
> statement about data egress that is not true if embedding is still running. Both the admin page and the refusal
> message say which capability is which for exactly this reason.

### Who can change it   {#who}
The application's own **Owner or Admin**, its organization's **Owner or Admin**, or a platform admin. It is stored in
the control plane, not in the application's model, and that placement is the point: an operator's decision must not
be something the application's own source can withdraw at its next compile.

It is also the only field pair on an application record that an app's own Owner may write — everything else there
stays with the organization tier. The reasoning: this is not administration of the app record, it is an operational
pull on what the app *does*, and the person accountable for that must be able to stop it without escalating.

### What a refused call looks like   {#refused}
The call fails. Over HTTP the response is **403** with the message:

```json title="generation switched off" syntax
{ "error": "Text generation is switched off for this application — an operator disabled the Generative AI capability, so no model call was made. This is not a budget limit and will not clear on its own. Embedding (semantic search) is switched separately and may still be running." }
```

403 rather than 500, because this is a decision somebody made rather than a fault; and rather than 429, because a
budget refusal clears at midnight and invites a retry while this one does not clear at all.

### When it takes effect   {#timing}
Within about **fifteen seconds**, in every process. The switch is read on every AI call, so it is cached briefly
rather than re-read from the control plane each time; the cache is time-bounded, so a change propagates everywhere
with nothing to deliver and nothing to go wrong in delivering it.

So "it is off" and "it will be off shortly" are different claims for a few seconds after you flip it. The admin page
says so.

### What it does NOT do   {#limits}
Two things worth knowing before you design around this.

**An application CAN ask whether AI is on** — `Ai.GenerationEnabled` and `Ai.EmbeddingEnabled`, two booleans read
on the server:

```osy title="hide the affordance instead of failing on it" test app=agent-ai-switch-hide
using Osysharp.Ui;

[Page("/assistant")]
[Render(CSR)]
[AllowAnonymous]
component Assistant() {
  live var aiOn = Ai.GenerationEnabled;

  render {
    if (aiOn) { Text("Ask the assistant anything."); }
    else      { Text("The assistant is switched off for this application."); }
  }
}
```

⛔ **This is advisory, never a gate.** The authority is the refusal above: the read has crossed to a client and can
be up to fifteen seconds stale, so a page that hides a button is being polite, not enforcing anything. What makes
the pair trustworthy is that both read the same switch — the button and the refusal cannot disagree about the
answer, only about how fresh it is.

⚠ And it is read when the page loads, not pushed. A page already open when the switch is flipped keeps showing the
button until it reloads; pressing it then gives the refusal. If you need the page to react live, that is a
different mechanism.

**The refusal is not a catchable Osy# exception.** It is not a member of the language's closed exception set, so
`catch (…)` in Osy# cannot name it specifically. It surfaces as a call failure carrying the message above.

**Rows already embedded were already sent.** Turning embedding off stops future calls; it does not recall anything.
If the concern is where data has gone rather than where it is going, this switch is not the whole answer — see
[embedding provider (app.Embedding)](https://osysharp.com/reference/config/embedding/) for running the model somewhere you choose instead.

## Examples       {#examples}

An operator turns generation off for one application and leaves search working:

```text title="one capability off, the other left running"
app-detail → AI
   ▢ Text generation      ← unticked
   ☑ Embedding
   "Generation is off. Embedding still runs, so indexed rows are still sent to whoever hosts that model."
```

The application's next model call fails with the 403 above. Its semantic search keeps answering.

### The dial beside the switch   {#budget}
The switch is binary. The [budget](https://osysharp.com/reference/agent/llm-budget/) is the same control plane's dial: hard daily limits in
tokens and dollars, per organisation, per application and per user, enforced before a call leaves and fail-closed.
An application that handles the switch's refusal handles the budget's the same way — both are the model not
answering, and the degraded path is one path.

## See also       {#see-also}
- [LLM budgets (hard daily limits per organisation, app and user)](https://osysharp.com/reference/agent/llm-budget/) — the dial beside this switch: hard daily budgets per organisation, app and user
- [default LLM model (app.DefaultModel)](https://osysharp.com/reference/agent/default-model/) — the model that is being switched off, and where it runs
- [embedding provider (app.Embedding)](https://osysharp.com/reference/config/embedding/) — the embedding model, its endpoint, and why placement is a requirement rather than a preference
- [Agents (calling a model like anything else you declared)](https://osysharp.com/reference/agent/index/) — the rest of the agent and model surface


---

<!-- https://osysharp.com/reference/agent/task-watch/ -->

# watching a task run (task.Watch)

> Everything a task has done so far, then everything it does next, as one stream that ends when the task does. A watcher can arrive late, leave, and come back — the work is driven by the task, not by whoever is looking at it, so watching costs the task nothing and stopping watching costs it nothing either.

<!-- id: agent-task-watch · area: agent · stability: preview · html: https://osysharp.com/reference/agent/task-watch/ -->

## Summary        {#summary}

```osy syntax
foreach (var step in task.Watch()) {
  // step.Text — "Using Search", "recommending approval"
}
```

One `foreach`. It replays what already happened, keeps going as more happens, and finishes when the task reaches a
terminal state. **Arriving late is not a special case** — a watcher who opens a task halfway through sees the same
thing as one who was there from the start.

## Signature      {#signature}

```osy syntax
task.Watch()   // → stream<AgentStep>
```

| `AgentStep` | |
|---|---|
| `Sequence` | order within the whole job, from 1 |
| `Kind` | `Said` · `ToolCall` · `ToolResult` · `Asked` · `Answered` · `Finished` |
| `Text` | the line a progress view shows |
| `Detail` | the tool name, the terminal state, the failure |
| `IsError` | the tool failed (`ToolResult` only) |
| `At` | when it happened |

⚠ It is a **method, not a property** — the parens are carrying meaning. [`task.Calls`](https://osysharp.com/reference/agent/task-calls/) is a
*value*, complete when you get it; this opens something with a lifetime.

## Description    {#description}

### Watching is free, and so is looking away   {#detach}

The task's work is driven by its [loop](https://osysharp.com/reference/agent/loop/), not by you. So:

- **open it** and you see everything so far, then everything next;
- **close it** and the task carries on exactly as before;
- **come back** and call `Watch()` again — it catches you up.

There is no cursor to keep, no reconnect to write, and no state on the client at all. That is the whole reason this
is a single member rather than a history call plus a live subscription: the moment they are two, every caller has to
join them, and get the join right.

⚑ A watcher must live inside something that can pass items on as they arrive — so a function reading a stream is
itself declared `stream<T>`. The compiler enforces this, which is what stops a watcher being written as an ordinary
function that quietly blocks until the task ends.

```osy syntax
stream<string> Progress(Guid taskId) {
  var task = ReviewTask.Where(t => t.Id == taskId).FirstOrDefault();
  foreach (var step in task.Watch()) {
    yield return step.Text;
  }
}
```

### Does it cover child tasks too?   {#subtree}

A task driven by a loop makes no model call itself — the run it starts is a child task, and the work is recorded
against the child. `Watch()` therefore covers the task **and everything beneath it**, which is what makes it show
anything at all on the task you were handed. Same reason [[agent-task-calls#allcalls|`AllCalls`]] exists.

### It ends when the task ends   {#terminal}

Completed, failed, or [stopped](https://osysharp.com/reference/agent/task-stop/) — the stream finishes and the `foreach` exits. A task that is
`Waiting` on a person is **not** terminal and the stream stays open: that is the case a watcher most wants to be
attached for, because the thing being waited on is usually them.

### Who can read it   {#security}

**If you can read the task, you can read its progress.** Steps hang off the task, so there is no second rule to
declare and none to forget:

```osy syntax
partial entity AgentTask {
  security { allow read when IsAuthenticated; }
}
```

⚠ A step says what the agent **did** — "Using Search", "recommending approval". It never carries what a tool
returned, because a tool result is the agent's own read performed with the *agent's* authority. That is
[`task.Transcript`](https://osysharp.com/reference/agent/task-transcript/), behind its own gate.

## Examples       {#examples}

```osy title="watching-a-review" test app=agent-task-watch
using Osysharp.Agents;

[Principal] entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read when IsAuthenticated; }
}

entity ReviewTask : AgentTask {
  security { allow read, update when IsAuthenticated; }
}

/// A progress feed a screen can bind straight to.
stream<string> Progress(Guid taskId) {
  var task = ReviewTask.Where(t => t.Id == taskId).FirstOrDefault();
  foreach (var step in task.Watch()) {
    yield return step.Sequence.ToString() + ". " + step.Text;
  }
}
```

## See also       {#see-also}
- [stopping work (task.Stop)](https://osysharp.com/reference/agent/task-stop/) — ending work that is still running, including the model call it is in
- [what a task cost, and what it did (task.Calls)](https://osysharp.com/reference/agent/task-calls/) — what the same work *cost*
- [what the agent saw (task.Transcript)](https://osysharp.com/reference/agent/task-transcript/) — what the agent actually read and wrote, behind its own gate


---

<!-- https://osysharp.com/reference/agent/task-calls/ -->

# what a task cost, and what it did (task.Calls)

> Every model call an agent task paid for, read off the task itself — the model, the turn, the tokens, the cache hits, the cost, how long it took, and whether it failed. `Calls` is what that one step spent; `AllCalls` is what the whole job spent, including the work it set off. Totals are ordinary LINQ over the list, so there is no stored number to drift from the rows it summarises.

<!-- id: agent-task-calls · area: agent · stability: preview · html: https://osysharp.com/reference/agent/task-calls/ -->

## Summary        {#summary}

A task tells you an agent did some work. This tells you what that work *cost* and what it consisted of.

```osy syntax
var spend = task.AllCalls.Sum(c => c.Cost);        // what the whole job cost
var slow  = task.AllCalls.Where(c => c.DurationMs > 5000);
var broke = task.AllCalls.Where(c => c.Error != null);
```

Each entry is one call to a model. Because the cost is **on each call**, every total you might want is a `Sum` you
write yourself — there is no stored figure that could disagree with the calls behind it.

## Signature      {#signature}

```osy syntax
task.Calls      // → List<AgentLlmCall>   this task's own calls
task.AllCalls   // → List<AgentLlmCall>   this task and everything beneath it
```

| `AgentLlmCall` | |
|---|---|
| `Model` | the model that answered — the tier actually used, not the one declared |
| `Turn` | which turn of the run this was (1-based) |
| `InputTokens` · `OutputTokens` | tokens sent and generated |
| `CacheCreationTokens` · `CacheReadTokens` | cache written, and cache read — reads are the ones that save money |
| `Cost` | what this call cost, in your currency units, as a `decimal` |
| `DurationMs` | wall-clock time for the provider call |
| `Truncated` | the model hit its token ceiling and was cut off mid-answer |
| `Error` | the failure, or null. A task whose calls carry errors spent money without delivering |
| `At` | when the request went out |
| `Task` | which task in the subtree made the call — so a total can be broken down by step |

## Description    {#description}

### `AllCalls` is almost always the one you want   {#allcalls}
⚠ **A task driven by a [loop](https://osysharp.com/reference/agent/loop/) makes no model calls itself.** The loop's task is the parent; the agent
run it starts is a child task, and the calls are recorded against the *child*. So on a loop-driven task:

```osy syntax
task.Calls.Sum(c => c.Cost)      // 0.00 — the parent spent nothing
task.AllCalls.Sum(c => c.Cost)   // what the job actually cost
```

That is why they are two members rather than one with a flag: a reader must not have to work out which they are
holding. Reach for `Calls` when you specifically want *this step's* spend and not its children's.

### The totals are yours to write   {#totals}
There is no `task.Cost`. The task deliberately stores no total, because a stored number and the rows it summarises
disagree the first time something fails halfway — and then nothing can say which is right. Summing the calls cannot
drift, and it gives you every other question for free:

```osy syntax
var spend    = task.AllCalls.Sum(c => c.Cost);
var turns    = task.AllCalls.Count;
var cached   = task.AllCalls.Sum(c => c.CacheReadTokens);
var priciest = task.AllCalls.OrderByDescending(c => c.Cost).FirstOrDefault();
```

⚑ Cost is converted once from the platform's internal whole-number units, so summing a list of these is exact. A
per-call rounded figure would not be.

### Who can read it   {#security}
**If you can read the task, you can read its calls.** There is no second rule to declare and none to forget: you can
only ask about a task you are holding, and you could only be holding one your app's own rule on
[`AgentTask`](https://osysharp.com/reference/agent/task-log/) allowed you.

So the decision is the one you already made:

```osy syntax
partial entity AgentTask {
  security { allow read when IsFinance; }     // …and only finance sees what anything cost
}
```

⚠ **Prompts and responses are NOT here.** These entries say what a call *cost*, never what it *contained*. That is
deliberate: an agent often runs with more authority than the person reading the screen, so its prompts can hold data
that reader is not entitled to. Exposing content through the same member as cost would make one read rule the only
thing standing between them. Content is [`task.Transcript`](https://osysharp.com/reference/agent/task-transcript/), behind a gate the agent
declares for itself.

## Examples       {#examples}

A review screen — the spend, and the calls behind it:

```osy title="what-a-task-cost" test app=agent-task-calls
using Osysharp.Agents;

[Principal] entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read when IsAuthenticated; }
}

entity ReviewTask : AgentTask {
  security { allow read when IsAuthenticated; }
}

/// What one task spent, and what it did to spend it.
string Spend(Guid taskId) {
  var task = ReviewTask.Where(t => t.Id == taskId).FirstOrDefault();
  if (task == null) { return "no such task"; }

  var calls  = task.AllCalls;
  var cost   = calls.Sum(c => c.Cost);
  var failed = calls.Where(c => c.Error != null).Count();

  return calls.Count.ToString() + " calls, " + cost.ToString("C")
       + (failed > 0 ? " (" + failed.ToString() + " failed)" : "");
}
```

## See also       {#see-also}
- [the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/) — the task itself: what caused it, when it ran, how it ended
- [what an agent hands back (AgentDeliverable)](https://osysharp.com/reference/agent/deliverables/) — what the agent chose to hand back, as opposed to what it spent
- [what the agent saw (task.Transcript)](https://osysharp.com/reference/agent/task-transcript/) — what those calls *contained*, and who may read it
- [the agent loop (app.Agent, Loop)](https://osysharp.com/reference/agent/loop/) — why a loop-driven task's own calls are empty


---

<!-- https://osysharp.com/reference/agent/deliverables/ -->

# what an agent hands back (AgentDeliverable)

> An agent presents its outcome as a LIST of deliverables, not a sentence: documents it wrote, files it produced, and records it created, in the order it chose. It records each one deliberately, by calling a tool for it, so the list is what the agent means to hand over rather than everything it happened to touch. The rows are written by the platform and can never be created, edited or deleted by app code; your app decides who may READ them, and builds the screens that show them.

<!-- id: agent-deliverables · area: agent · stability: stable · html: https://osysharp.com/reference/agent/deliverables/ -->

## Summary        {#summary}

A task's `Outcome` is one sentence — enough for a list view, and never enough to act on. The deliverables are what
the sentence is about.

Each thing an agent hands back is one `AgentDeliverable` row hanging off its task: a **document** it wrote, a **file**
it produced, or a **record** it created. A task has as many as it needs, in the order the agent chose, and the three
shapes sit in one list — so a screen can show a summary, the memo behind it and the twelve receipts that were filed,
together.

`AgentDeliverable` arrives with `using Osysharp.Agents;`, alongside [the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/).

## Signature      {#signature}

```osy syntax
using Osysharp.Agents;

partial entity AgentDeliverable {
  security { allow read when IsAuthenticated; }
}
```

The fields you will read:

| field | what it holds |
|---|---|
| `Task` | the work this is an outcome of |
| `Kind` | `Document`, `File` or `Record` — which of the three below carries the content |
| `Sequence` | the agent's own ordering, from 0 |
| `Label` | what this IS, in a few words — the line a reviewer scans, and what groups related rows |
| `Document` | `Kind = Document`: the markdown the agent wrote |
| `FileAsset` | `Kind = File`: the file it produced |
| `EntityTypeName` · `EntityId` | `Kind = Record`: the record it created |

And from the other side, on the task itself:

| field | what it holds |
|---|---|
| `AgentTask.Deliverables` | every outcome of that task, as a collection |

## Description    {#description}

**A list, because a sentence cannot carry an answer.** *"Checked the report and filed one receipt"* tells a reviewer
nothing they can open. The deliverables are the openable half: the policy check they can read, the memo they can
download, the draft they can approve.

**One row per thing, and grouping is yours.** Twelve filed receipts are twelve rows sharing a `Label`, not one row
that mentions twelve. A list can always be grouped for display; a group cannot be ungrouped, and the individual rows
are what a reviewer clicks.

**The agent records these DELIBERATELY, and that is what makes the list worth reading.** The platform separately
knows everything a task touched — that is the audit trail, and it is complete. This list is different: it holds only
what the agent chose to present. Most rows an agent touches on the way to an answer are working material, and a list
derived from them would bury the answer in bookkeeping.

⚑ **So an agent that does the work and presents nothing leaves an empty list**, and that is a finding worth showing
rather than an error. It means the work happened and nobody can see what came of it. The platform will not refuse to
finish such a task — an agent whose honest answer is "nothing to report" has to be able to say so.

**A document is markdown, and that buys more than formatting.** It is stored as a real document with sections, so it
can be read section by section, edited afterwards, and — because it is indexed for recall — found later by what it
SAYS. *"What did we decide about the Lisbon policy"* can find the deliverable that decided it, months on. A plain
string would be a dead end in exactly the place it is most useful.

**A deliverable never points at nothing.** Recording a record or a file that does not exist is refused at the moment
the agent tries, while the agent is still working and can fix it — rather than becoming a card on a review screen
that looks live and opens nothing.

**The rows are written by the platform.** App code can never create, change or delete an `AgentDeliverable`: a
fabricated one would claim an agent produced something it did not, and a deleted one would hide what it did produce.
Attempting a write is refused.

**Who may READ them is entirely yours**, exactly as for the task log — a plain `using Osysharp.Agents;` gives you a
table nobody can read yet, and you declare the rule you want. The review screen, the approval queue and the activity
dashboard are your app's to build; these rows are the material.

## Examples       {#examples}

An app that lets any signed-in person read agent work and everything it produced:

```osy title="agent-deliverables-read-rule" test app=agent-deliverables
using Osysharp.Agents;

[Principal] entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read when IsAuthenticated; }
}

partial entity AgentTask {
  security { allow read when IsAuthenticated; }
}

partial entity AgentDeliverable {
  security { allow read when IsAuthenticated; }
}
```

With both rules in place, a task's outcomes are an ordinary collection on an ordinary entity: read
`task.Deliverables`, order by `Sequence`, and render each row by its `Kind`.

## See also       {#see-also}
- [the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/) — the work itself: what caused it, when it ran, what it cost.
- [agent conversation memory (using Osysharp.Agents)](https://osysharp.com/reference/agent/conversation-memory/) — what the agent SAID, as against what it handed over.
- [running an agent from your code](https://osysharp.com/reference/agent/ask/) — running an agent from your own code.


---

<!-- https://osysharp.com/reference/agent/task-transcript/ -->

# what the agent saw (task.Transcript)

> What an agent's model calls actually contained — the system prompt as the model received it, the messages sent, the response that came back, and every tool the turn invoked with its input and output. Unlike a task's costs, content is not readable just because you can read the task: the agent declares who may see what it saw, and an agent that declares nobody is readable by nobody.

<!-- id: agent-task-transcript · area: agent · stability: preview · html: https://osysharp.com/reference/agent/task-transcript/ -->

## Summary        {#summary}

[`task.Calls`](https://osysharp.com/reference/agent/task-calls/) tells you what a task *cost*. This tells you what it *contained*.

```osy syntax
foreach (var turn in task.Transcript) {
  Log.Information("turn {N}: {Response}", turn.Turn, turn.Response);
}
```

⛔ **This is the one agent surface with a gate of its own**, and it is worth knowing why before you use it. An agent
usually runs with **more authority than the person reading the screen** — an auditing agent may read every line of
every report, while the employee looking at the task may read only their own. Everything the agent read is in its
prompt. So content does not ride the task's read rule; the agent says who may see it, and says it by default to
nobody.

## Signature      {#signature}

```osy syntax
task.Transcript   // → List<AgentLlmTurn>
```

| `AgentLlmTurn` | |
|---|---|
| `Turn` | which turn of the run this was (1-based) — pairs with `AgentLlmCall.Turn` |
| `Model` | the model that answered |
| `SystemPrompt` | the instructions **as the model received them**, after every interpolation |
| `Messages` | the messages sent, as JSON text |
| `Response` | what came back — text and tool requests, in the order the model produced them, as JSON text |
| `At` | when the request went out |
| `ContentWithheld` | the app chose not to record this turn's content — see [[agent-task-transcript#withheld|when the content was never recorded]] |
| `Tools` | what this turn invoked, in order. Empty on a turn that only talked |

| `AgentToolCall` | |
|---|---|
| `Name` | the tool as the model named it |
| `Input` | the arguments the model generated, as JSON text |
| `Output` | what the tool returned to the model, as JSON text |
| `IsError` | the tool failed |
| `DurationMs` | wall-clock time for the tool itself |
| `Sequence` | order within the turn (1-based) |

## Description    {#description}

### Who can read it — you must say, or nobody can   {#security}

Say it on the agent, in its `security { }` block:

```osy syntax
agent Auditor {
  Prompt = "You review expense reports…";
  Roles  = [Role.Finance];                              // what makes its prompts finance-scoped

  security { allow read Transcript when IsFinance; }    // who may read what it saw
}
```

**Omit the rule and the transcript is unreadable — by everyone, including the roles the agent itself holds.** That
is deliberate. A gate that defaulted to "readable" would make forgetting the rule indistinguishable from deciding
you did not need one, on the one surface where those must not look alike.

The rule is written on the *agent* rather than on [`AgentTask`](https://osysharp.com/reference/agent/task-log/) because the exposure belongs to the
agent: `Roles = [Role.Finance]` is the line that puts finance-scoped data into its prompts, and the gate sits three
lines below it. Two agents in the same app can hold very different authority, and a single app-wide switch would
have to be as strict as the most privileged one — or leak through the loosest.

⚠ Its predicate is about the **caller**, so it takes `when`, not `where`. Name a policy (`when IsFinance`) or write
the condition inline; there is no row to filter.

### Does it include child tasks' turns?   {#subtree}

`Transcript` includes the turns of everything the task set off, not just its own — because a task driven by a
[loop](https://osysharp.com/reference/agent/loop/) makes no model call itself, so "its own" would be empty for exactly the tasks you most want to
inspect. This is the same reason [[agent-task-calls#allcalls|`AllCalls`]] exists, which is why there is no
`AllTranscript` to choose between.

⛔ **If any agent involved refuses you, you get nothing at all** — not the turns you would have been allowed. A
transcript with some turns quietly missing cannot be told apart from an agent that simply said little, and a reader
would have no way to know they were looking at a partial record. Where a job spans agents with different gates, that
means you need clearance from each.

### When the content was never recorded   {#withheld}

A turn whose `ContentWithheld` is true happened and cost what [its call](https://osysharp.com/reference/agent/task-calls/) says it cost, but its
prompt and response were never written down. Three things cause it:

- the agent declares `Logging = MetadataOnly`;
- the app turned the call record off entirely;
- the platform dropped the body because it carried data at a redacted classification.

Show it. A blank prompt with no explanation reads as a broken screen:

```osy syntax
foreach (var turn in task.Transcript) {
  if (turn.ContentWithheld) { /* "not recorded" */ }
  else { /* turn.SystemPrompt, turn.Messages, turn.Response */ }
}
```

## Examples       {#examples}

A review screen showing what the agent was told and what it did about it:

```osy title="what-the-agent-saw" test app=agent-task-transcript
using Osysharp.Agents;

[Role] enum Role { Finance, Staff }

[Principal] entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read when IsAuthenticated; }
}

entity RoleGrant {
  [Required] User User;
  [Required] Role Role;
  security { allow read when IsAuthenticated; }
}

policy IsFinance => RoleGrant.Any(g => g.User == user && g.Role == Role.Finance);

/// Anyone signed in may see that the review happened, and what it cost.
entity ReviewTask : AgentTask {
  security { allow read when IsAuthenticated; }
}

agent Auditor {
  Purpose   = "Review a submitted expense report.";
  Prompt    = "You review expense reports against the travel policy.";
  Principal = new User { Email = "auditor@ledger.demo" };
  Roles     = [Role.Finance];

  /// …but only finance sees what it read to do so.
  security { allow read Transcript when IsFinance; }
}

/// What the agent was told, and which tools it reached for.
string WhatItSaw(Guid taskId) {
  var task = ReviewTask.Where(t => t.Id == taskId).FirstOrDefault();
  if (task == null) { return "no such task"; }

  var turns = task.Transcript;
  if (turns.Count == 0) { return "not available to you"; }

  var tools = 0;
  foreach (var turn in turns) { tools = tools + turn.Tools.Count; }

  return turns.Count.ToString() + " turns, " + tools.ToString() + " tool calls";
}
```

## See also       {#see-also}
- [what a task cost, and what it did (task.Calls)](https://osysharp.com/reference/agent/task-calls/) — what the same calls *cost*, readable by anyone who can read the task
- [the agent task log (AgentTask)](https://osysharp.com/reference/agent/task-log/) — the task itself: what caused it, when it ran, how it ended
- [the agent loop (app.Agent, Loop)](https://osysharp.com/reference/agent/loop/) — why a loop-driven task's own calls are empty, and this member spans the job


---

<!-- https://osysharp.com/reference/api/rest/ -->

# publishing a REST API (app.Apis)

> `app.Apis` publishes your app to a third party over HTTP. You never write a controller or an endpoint handler — you EXPOSE what already exists: `Expose` turns an entity into CRUD routes (`new Crud<Order>() { Operations = [...] }`), and `Endpoints` maps one of your functions to a route (`new Endpoint(SubmitOrder) { Method = HttpMethod.Post, Path = "/submit" }`). Each `RestApi` has a `Route` prefix and an optional `Auth` (API key and/or bearer). The entity in a `Crud<T>` and the function in an `Endpoint(...)` must exist — a name that doesn't resolve is a compile error that names what does.

<!-- id: api-rest · area: api · stability: stable · html: https://osysharp.com/reference/api/rest/ -->

## Summary        {#summary}
`app.Apis` publishes your application over HTTP for other systems to call. The guiding idea is that you **expose what
you already have** rather than write a web layer: you do not author controllers, routes, DTOs or handlers. Two things
can be published — an **entity**, as a set of CRUD routes, and a **function**, mapped to a single route. Each API has a
`Route` prefix and an optional `Auth` gate.

```osy syntax
app.Apis = [
  new RestApi("Orders") {
    Route   = "orders",
    Auth    = new ApiAuth { ApiKey = true },
    Expose  = [ new Crud<Order>() { Operations = [CrudOp.Read, CrudOp.Create] } ],
    Endpoints = [ new Endpoint(SubmitOrder) { Method = HttpMethod.Post, Path = "/submit" } ],
  },
];
```

## Signature      {#signature}
```osy syntax
app.Apis = [
  new RestApi("Name") {          // one entry per published API
    Route   = "orders",          // the URL prefix for every route below
    Version = "1.0",             // optional — omit and it defaults to v1
    Auth    = new ApiAuth { ApiKey = true, Bearer = true },   // optional gate
    Expose  = [                  // entities → CRUD routes
      new Crud<Order>() { Operations = [CrudOp.Read, CrudOp.Create, CrudOp.Update, CrudOp.Delete] },
    ],
    Endpoints = [                // functions → one route each
      new Endpoint(SubmitOrder) { Method = HttpMethod.Post, Path = "/submit", SuccessStatus = 201 },
    ],
  },
];
```

`app.Apis` is a list — an app may publish several independent APIs, each with its own `Route` and `Auth`.

## Description    {#description}
A `RestApi` has:

- **`Route`** — the URL prefix under which its routes live.
- **`Version`**, **`Deprecated`**, **`Sunset`** *(optional)* — API lifecycle metadata. Every route lives under a
  `/v{major}/` prefix; an omitted `Version` defaults to **v1**, so the simple case needs no version at all. A
  multi-major API gives each `RestApi` its own `Version` (e.g. `"1.0"`, `"2.0"`).
- **`Auth`** *(optional)* — an `ApiAuth { ApiKey = true, Bearer = true, OAuth = true }`. Those three flags are its
  WHOLE vocabulary; it names no secret and takes no other member. Absent, the API is unauthenticated. Who each of
  them makes the caller is the next section — read it before you write a `security {}` rule for an API.
- **`Expose`** — a list of `new Crud<Entity>() { Operations = [CrudOp.…] }`. Each entry turns one entity into REST
  routes; `Operations` chooses which of `Read` / `Create` / `Update` / `Delete` exist. The entity must be defined in
  your app — a `Crud<Unknown>` is a compile error that lists the entities you *do* define.
- **`Endpoints`** — a list of `new Endpoint(Function) { Method = HttpMethod.…, Path = "…" }`. Each maps one of your
  functions to a route. The function must be declared — an `Endpoint(NoSuchFn)` is a compile error that lists your
  functions.

You write no request parsing, no serialization, and no routing table: the shape of the entity and the signature of the
function are the contract.

**Refusals map to the right status.** When an endpoint's function *refuses* — it `throw`s a typed exception, or a
declared security policy denies the caller — the response carries the matching 4xx, not a blanket 500, and the client
sees the refusal's own message:

| The function… | Response |
|---|---|
| `throw new ValidationException("…")` | `400` — the request was invalid |
| `throw new NotFoundException("…")` | `404` — the target does not exist / is not visible |
| `throw new ConflictException("…")` | `409` — conflicts with current state |
| is denied by a security / authorization policy | `403` — the caller may not do this |
| hits a genuine, unexpected error | `500` — a generic message; the details stay in the server log, never the response |

So a business rule like *"a backup can only be downloaded once it is ready"* is a `throw new ValidationException(…)`
that reaches the caller as a `400` with your wording — not a server crash.

### What a value looks like on the wire  {#wire-form}
JSON has no literal for most of what your entities hold, so each type has one agreed spelling — the same one going
out as coming in, which is what makes a response postable straight back:

| Declared as | On the wire |
|---|---|
| `enum` | its **member name** — `"machine": "Washer"`, never the ordinal `0` |
| `enum` with `[Value("…")]` | that **value**, because you chose it — `"risk": "risk-high"` |
| `Guid`, `DateTime`, `DateOnly`, `TimeOnly`, `TimeSpan` | a string in the canonical form — `"2026-09-04"`, `"01:30:00"` |
| `int`, `decimal`, `bool` | the JSON number or boolean, unquoted |

**An enum also accepts its stored key inbound** — the ordinal, or the `[Value]` of a string-backed enum — so a client
written against an older payload keeps working. The name is what a read ANSWERS and what the published OpenAPI
advertises; the key is a compatibility affordance.

Anything else is a **`400` naming the field and what it accepts**, with an enum's whole closed set spelled out:

```json title="a value outside the vocabulary" syntax
{ "error": { "code": "VALIDATION_FAILED", "details": [
  { "field": "machine", "code": "INVALID_ENUM_VALUE",
    "message": "'machine' expects one of Machine: Washer, Dryer — received a string ('Toaster')." } ] } }
```

### …and what the response is WRAPPED in  {#envelope}
The table above is about one VALUE. This is about the **object it arrives inside**, which is not always the value
itself — and every key in it is **camelCase**, whatever the member is called in your source.

| The route | What comes back |
|---|---|
| an `Endpoint` whose function returns an **entity** | that row, **flat** — `{"id": "…", "accession": "ACC-1"}` |
| an `Endpoint` whose function returns a **`class`** | its fields, **flat** — `{"accession": "ACC-1", "species": "Rana"}` |
| an `Endpoint` whose function returns **`void`** | `{"status": "ok"}` |
| an `Endpoint` whose function returns **anything else** — a `List<T>`, a `Dictionary<K,V>`, an `int`, a `string` | **wrapped**: `{"result": …}` |
| `Crud<T>` **read-many** (`GET /entities/T`) | `{"items": [ … ], "total": 12, "top": 50, "skip": 0, "hasMore": false}` — plus `"included"` when you asked to expand |
| `Crud<T>` **read-one / create / update** | that row, **flat** |
| `Crud<T>` **delete** | `204`, no body |

⚠ **The `result` wrapper is the one that catches people, because the obvious DTO is the one that does not work.** A
route whose function returns `List<Receipt>` does **not** answer a bare JSON array, so
`JsonSerializer.Deserialize<List<Receipt>>(body)` fails — the body is an object. Declare the wrapper instead, and
read through it:

```osy syntax
class ReceiptPage { public List<Receipt> Result; }   // `Result` binds the body's `result`

var page = JsonSerializer.Deserialize<ReceiptPage>(r.Body);
Assert.Equal(2, page.Result.Count);
```

[Calling your own REST API from a test](https://osysharp.com/reference/testing/api-calls/) has this as a compiled example, and [JsonSerializer](https://osysharp.com/reference/json/serializer/) is the verb that reads it.

## Who is `user`?  {#who-is-user}
Every rule you already wrote applies to an API call unchanged, because an authenticated API caller is **a user of your
app** — not a special anonymous "machine" identity. This is the one thing about `app.Apis` worth reading before you
design anything: guessing it the other way leads you to open your entities to anonymous callers so the API "works",
which opens them to the whole world on every other surface too.

**An API key is a PER-USER credential. There is no app-wide API key.** A key is minted against exactly one
`[Principal]` row, and a request carrying it runs **as that user**:

| `Auth` | the request carries | `user` | `IsAuthenticated` | `IsAnonymous` |
|---|---|---|---|---|
| `new ApiAuth { ApiKey = true }` | `X-API-Key: pk_…` | the key OWNER's principal row | `true` | `false` |
| `new ApiAuth { Bearer = true }` | `Authorization: Bearer …` | that token's principal row | `true` | `false` |
| `new ApiAuth { OAuth = true }` | an OAuth-issued bearer token | that token's principal row | `true` | `false` |
| any `Auth` at all | *nothing* | — | — | — → the request is **`401`** and never reaches your data |
| **no `Auth`** | anything | `null` | `false` | `true` |

So an API key and a bearer token for the same person produce **the same context**: the same `user`, the same role
grants, the same row filters. A rule like `allow read where OwnerEmail == user.Email` returns that person's rows over
the API exactly as it does in the app's own UI, and `allow read, create when IsAuthenticated` admits them.

Three consequences, each of which is a mistake if you assume the opposite:

- **You do not need `IsAnonymous` to make an entity reachable over an authenticated API.** The caller is somebody.
  An `allow … when IsAnonymous` rule is in fact the one thing an API-key caller can *never* satisfy.
- **A missing credential is a `401`, not an anonymous request.** `ApiAuth { ApiKey = true }` never falls through to
  anonymous access; the request stops at the door.
- **Only an API with no `Auth` runs anonymously**, and there `user` is null — so a row filter correlating to `user`
  matches nothing and denies, which is the correct answer for a caller who is nobody.

⚑ **You can check every row of that table against your own app rather than take it from here.** [Calling your own REST API from a test](https://osysharp.com/reference/testing/api-calls/)
sends a real request to your published route from inside a `[Test]`, with or without a credential, and hands you the
status back — so "an API key caller is authenticated" is a claim you can make the app answer:

```osy syntax
Assert.Equal(401, Api.Get("/api/rest/v1/catalog/entities/Product").StatusCode);                   // nobody
Assert.Equal(200, Api.Get("/api/rest/v1/catalog/entities/Product", apiKey: key).StatusCode);      // somebody
```

### Where the key lives  {#api-key-storage}
The platform stores each user's key as a **hash on that user's own principal row**, in two columns your `[Principal]`
entity must declare by these exact names:

```osy syntax
[Principal] entity User {
  [Unique, MaxLength(200)] string Email;
  [MaxLength(255)] string? ApiKeyHash;    // the key in force
  [MaxLength(255)] string? ApiKeyHash2;   // a second, so a key can be rotated without a gap
  security {
    allow read when IsAuthenticated;
    // ⛔ BOTH SLOTS. A stored key hash with no field-level `deny read` rides `Session.CurrentUser` to the
    //    browser — the platform ships the whole [Principal] minus its MASKED properties.
    deny read ApiKeyHash  when !IsAuthenticator;
    deny read ApiKeyHash2 when !IsAuthenticator;
  }
}
```

`IsAuthenticator` is a policy **you declare** over your own grant table — it is not built in. See
[auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) for the three lines that declare it, and [acting for another principal](https://osysharp.com/reference/security/acting-for-another-principal/) for
the whole shape worked through on an app whose API caller is a machine.

Declaring `Auth = new ApiAuth { ApiKey = true }` on an app whose principal lacks them is a **compile error** naming
both columns — without them no key can be minted and none can be verified, so the published route would be a `401`
forever.

You never write a key into source. The platform mints one for a named user and prints the plaintext `pk_…` **once**;
only its hash is stored, and the caller then presents it in the `X-API-Key` header:

```bash
osyrin app user apikey generate alice@example.com     # prints the pk_… once
osyrin app user apikey revoke   alice@example.com     # --secondary to drop only the rotation key
```

Because there are two columns, rotation has no gap: generate a second key while the first still works, move the
callers over, then revoke the old one.

⚑ **In a test, ask for one by naming the principal** — [[testing-api-calls#credentials|`Api.KeyFor(P)`]]. "You never
write a key into source" is about your APP's source and stays true; a `[Test]` mints a real one against its own
throwaway branch, so the gated route above can actually be exercised rather than only refused.

⚠ **`app.Secrets` is a different thing and is not involved.** `app.Secrets` declares secrets your app *consumes* — an
LLM key, an OAuth client secret. `ApiAuth` has no member that names a `Secret`, so there is nothing to declare there
for an API key, and declaring one does not gate anything.

## Examples       {#examples}
A complete app that publishes one entity as read/create CRUD and one function as a POST route:

```osy title="basic" test app=api-rest-example
entity Order {
  [Required, MaxLength(200)] string CustomerEmail;
  decimal Total;
}

decimal SubmitOrder(decimal total, decimal taxRate) {
  return total + total * taxRate;
}

app.Apis = [
  new RestApi("Orders") {
    Route  = "orders",
    Expose = [ new Crud<Order>() { Operations = [CrudOp.Read, CrudOp.Create] } ],
    Endpoints = [
      new Endpoint(SubmitOrder) { Method = HttpMethod.Post, Path = "/submit", SuccessStatus = 201 },
    ],
  },
];
```

Require an API key on every route. The key belongs to a USER, so the principal declares the two columns that hold
it — and the entity's `security {}` then governs the API caller exactly as it governs that same person signed in:

```osy title="with-api-key" test app=api-rest-authed
[Role] enum AppRole { Authenticator, Member }
entity RoleGrant {
  [Required("Name the user this grant belongs to.")] User Grantee;
  [Required("Choose the role this grant confers.")] AppRole Level;
  security { allow read when IsAuthenticated; }
}
policy IsAuthenticator => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Authenticator);

[Principal] entity User {
  [Unique, MaxLength(200)] string Email;
  [MaxLength(255)] string? ApiKeyHash;
  [MaxLength(255)] string? ApiKeyHash2;
  security {
    allow read when IsAuthenticated;
    // ⛔ BOTH SLOTS, or the stored key hash rides `Session.CurrentUser` to the browser.
    deny read ApiKeyHash  when !IsAuthenticator;
    deny read ApiKeyHash2 when !IsAuthenticator;
  }
}

entity Product {
  [Required, MaxLength(200)] string Name;
  decimal Price;
  security {
    allow read when IsAuthenticated;   // the API-key caller IS authenticated — no anonymous grant needed
  }
}

app.Apis = [
  new RestApi("Catalog") {
    Route  = "catalog",
    Auth   = new ApiAuth { ApiKey = true },
    Expose = [ new Crud<Product>() { Operations = [CrudOp.Read] } ],
  },
];
```

## See also       {#see-also}
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — `IsAuthenticated` / `IsAnonymous`, the predicates the table above resolves
- [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/) — signing a user in, for bearer-authenticated routes
- [function](https://osysharp.com/reference/function/declaration/) — the functions an `Endpoint` publishes
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — `app.Secrets`, for the secrets your app CONSUMES; an API key is not one of them
- [Calling your own REST API from a test](https://osysharp.com/reference/testing/api-calls/) — `Api.*`: calling these routes from your own `.test.osy`, and asserting the status each refusal answers


---

<!-- https://osysharp.com/reference/class/inheritance/ -->

# Class inheritance

> A class can derive from another class with `class Circle : Shape`, inheriting its fields and its methods to any depth. A value of the derived type goes wherever the base is expected, and a `virtual` method can be replaced by an `override` one. `sealed` closes a class to further derivation. A class may derive only from a class — never from an entity, and never the reverse.

<!-- id: class-inheritance · area: class · stability: stable · html: https://osysharp.com/reference/class/inheritance/ -->

## Summary        {#summary}
A class derives from another with `:`, exactly as in C#. It inherits the base's fields and methods:

```osy title="a derived class inherits fields and methods" test app=class-inheritance
class Shape {
  public string Name;

  public string Describe() {
    return Name;
  }
}

class Circle : Shape {
  public decimal Radius;
}

string DescribeACircle() {
  var c = new Circle { Name = "small", Radius = 2m };
  return c.Describe();          // the method Shape declares, called on a Circle
}
```

## Signature      {#signature}
```osy syntax
class Derived : Base { … }      // inherits Base's fields and methods
sealed class Leaf { … }         // no type may derive from Leaf
```

## Description    {#description}

### What does a subclass inherit?   {#what-is-inherited}
Fields and methods, to any depth. A three-level chain works the way it reads, and a member declared anywhere above is
available below:

```osy title="inheritance is transitive" test app=class-inheritance-depth
class Shape {
  public string Name;
}

class Round : Shape {
  public decimal Radius;
}

class Dot : Round { }

string NameOfADot() {
  var d = new Dot { Name = "tiny", Radius = 0m };
  return d.Name;                // declared two levels up
}
```

### A derived value fits a base slot   {#upcast}
This is the point of a hierarchy: code written against `Shape` accepts every shape. The conversion is implicit and
needs nothing written:

```osy title="a Circle goes wherever a Shape is expected" test app=class-inheritance-upcast
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

string NameThrough() {
  Shape s = new Circle { Name = "c", Radius = 2m };
  return s.Name;
}
```

It is **one-way**, as in C#. A `Shape` is not assignable to a `Circle`, because not every shape is one — going the
other way needs a [type test](https://osysharp.com/reference/class/type-tests/).

The same rule applies wherever two values meet and one type has to describe both — a conditional, a `switch`
expression, a `??` fallback. The answer is the **base** of the two:

```osy title="picking between a base and a derived value" test app=class-inheritance-upcast-positions
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

string Pick(bool round, int kind) {
  var c = new Circle { Name = "circle", Radius = 2m };
  var p = new Shape { Name = "plain" };

  Shape a = round ? c : p;              // the branches unify to Shape
  Shape b = kind switch { 1 => c, _ => p };   // so do the arms
  Shape d = a ?? c;                     // and so do the sides of `??`
  return a.Name + b.Name + d.Name;
}
```

Because the result is a `Shape`, only `Shape`'s members are readable through it — reach for a
[type test](https://osysharp.com/reference/class/type-tests/) to get back to `Radius`.

**Two SIBLINGS take the type they are written into.** A `Circle` and a `Square` are both `Shape`s, but neither is the
other, so there is no type to *infer* — and where the type is *written*, that is the answer:

```osy title="the target types the conditional" test app=class-inheritance-target-typed
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }
class Square : Shape { public decimal Side; }
class Plot { public Shape Primary; }

string Show(Shape s) { return s.Name; }

string Pick(bool round) {
  var c = new Circle { Name = "circle", Radius = 1m };
  var q = new Square { Name = "square", Side = 2m };

  Shape s = round ? c : q;                        // a declared local
  var p = new Plot { Primary = round ? c : q };   // a member
  return Show(round ? c : q) + s.Name + p.Primary.Name;   // an argument
}
```

It works wherever the target is written: a declared local, a parameter, a member, an assignment, a `return`, and the
arms of a `switch` expression. What it does **not** do is invent one — `var s = round ? c : q;` writes the value
into nothing, so there is nothing to take, and the compiler says so and names the fix. (Same in C#.)

### Can a `Circle[]` be used as a `Shape[]`?   {#covariance}
A `Circle[]` **is** a `Shape[]`, and needs nothing written — as in C#. This holds in every position: a local, a
parameter, a return, a class member.

```osy title="Circle[] flows into Shape[]" test app=class-inheritance-array-covariance
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

int CountShapes(Shape[] shapes) { return shapes.Count(); }

int HowMany() {
  Circle[] cs = [new Circle { Name = "a", Radius = 1m }, new Circle { Name = "b", Radius = 2m }];
  Shape[] xs = cs;
  return CountShapes(xs);
}
```

A **`List<T>` is different, and deliberately so**: it can be appended to, so a `List<Circle>` is *not* a
`List<Shape>`. Handing one over would let the receiver add a plain `Shape` to your list of circles. If the receiver
only reads, declare it `Shape[]` — a `List` passes straight into a read-only sequence.

**An array of a type that HAS subtypes cannot be written through.** This is the other half of the rule above, and
the reason the conversion is safe: since a `Shape[]` may really be a `Circle[]`, storing a plain `Shape` into one
would leave an element missing the members the narrower array promises.

```osy title="✗ writing into an array that may be a Circle[]" syntax
Shape[] xs = circles;              // fine — read it all you like
xs[0] = new Shape { Name = "x" };  // refused: `Shape` has subtypes, so this array may be a `Circle[]`
```

C# allows that write and throws `ArrayStoreException` when it runs; here it is the same rule, moved to where you can
see it. An array of a type nothing derives from — including every scalar array, `int[]`, `double[]`, `string[]` — is
written exactly as in C#. When you need to write into a polymorphic sequence, use a `List<Shape>`: it is invariant,
which is what makes it safe to write.

### `sealed` closes the class   {#sealed}
`sealed` says no type may derive. Write it when a class is meant to be the end of its line:

```osy syntax
sealed class Candle : Mark { }  // deriving from Candle is a compile error
```

### Specialising a method: `virtual` and `override`   {#override}
A base method marked `virtual` may be replaced by a derived one marked `override`. The call runs the method of the
value's **actual** type, whatever type the slot holding it is declared as:

```osy title="the derived body runs through a base-typed slot" test app=class-inheritance-override
class Shape {
  public string Name;
  public virtual decimal Area() { return 0m; }
}

class Circle : Shape {
  public decimal Radius;
  public override decimal Area() { return Radius * Radius * 3m; }
}

decimal AreaThroughTheBase() {
  Shape s = new Circle { Name = "c", Radius = 2m };
  return s.Area();              // 12 — Circle's body, not Shape's
}
```

**Both words are required, each for a different mistake.** Without `virtual` on the base, adding a method to a base
class would silently change what every subclass sharing that name does. Without `override` on the derived one, an
accidental name collision would read as a deliberate specialisation. Redeclaring a method that is not `virtual` is
refused, and the refusal names the word that is missing.

**`base.M()` calls the version the derived class inherits** — what makes an override able to EXTEND the base rather
than replace it:

```osy title="an override that builds on its base" test app=class-inheritance-base-call
class Shape {
  public string Name;
  public virtual decimal Area() { return 2m; }
}

class Circle : Shape {
  public decimal Radius;
  public override decimal Area() { return base.Area() + 1m; }   // 3 — Shape's answer, plus one
}

decimal AreaOfACircle() {
  Shape s = new Circle { Name = "c", Radius = 1m };
  return s.Area();
}
```

`base` looks up the chain, not just one step: if the immediate parent declares nothing by that name, the call runs
the nearest ancestor that does. It is only meaningful inside a class member's body, and a local variable named
`base` shadows it.

### Constructing the base: `: base(…)`   {#base-constructor}
A derived class's constructor runs the base class's constructor first, and says which one with `: base(…)`:

```osy title="the base constructor runs first" test app=class-inheritance-base-ctor
class Shape {
  public string Name;
  public decimal Width;
  public Shape(string n, decimal w) { Name = n; Width = w; }
}

class Circle : Shape {
  public decimal Radius;
  public Circle(string n, decimal r) : base(n, r * 2m) { Radius = r; }
}

string BuildOne() {
  var c = new Circle("small", 2m);
  return c.Name;                 // "small" — set by Shape's constructor
}
```

The arguments are an ordinary argument list: as many as the base constructor takes, in any expression, and by name
(`: base(n, w: 4m)`) if you prefer. `: base()` is how you call a parameterless base constructor explicitly.

A constructor with **no** initializer runs the base's *parameterless* constructor, exactly as in C#. So if the base
declares a constructor that takes arguments, the derived one has to say what to pass — and the compiler asks for it
by name. A base class with no constructor at all needs nothing: its fields take the defaults their declarations give.

Members the base constructor assigns count as assigned, so you do not have to supply them again at the create site.

`: this(…)` — chaining to another constructor of the same class — needs constructor overloading, which is not
available yet.

### `abstract` — a shape to derive from   {#abstract}
An `abstract class` cannot be created; it exists for other types to fill in. An `abstract` method declares **what** a
subclass must provide and has no body:

```osy title="the obligation, and the class that meets it" test app=class-inheritance-abstract
abstract class Shape {
  public string Name;
  public abstract decimal Area();        // no body — every Shape has one, but Shape does not say what
}

class Circle : Shape {
  public decimal Radius;
  public override decimal Area() { return Radius * Radius * 3m; }
}

decimal AreaOfACircle() {
  Shape s = new Circle { Name = "c", Radius = 2m };
  return s.Area();                       // 12 — Circle's body
}
```

An abstract method is already the thing a subclass overrides, so it needs no `virtual`. The first **concrete** class
below it must provide a body — a class that does not is asked for one by name, unless it is `abstract` too, in which
case the obligation passes down. `abstract` and `sealed` are opposites and cannot both be written: one says the type
must be derived from, the other that it must not.

### `protected` — visible down the chain, and nowhere else   {#protected}
A class member is `private` unless it says otherwise, and `private` means *this class only* — a subclass cannot see
it. `protected` is the middle setting: reachable from the declaring class and from anything that derives from it.

```osy title="a subclass reads it; nothing else can" test app=class-inheritance-protected
class Shape {
  protected string Tag;                      // subclasses may read and write it
  private string secret;                     // this class only, even for a subclass

  public Shape(string t) { Tag = t; secret = "hidden"; }
  protected string Describe() { return "[" + Tag + "]"; }   // methods take it too
}

class Circle : Shape {
  public decimal Radius;
  public Circle(string t, decimal r) : base(t) { Radius = r; }
  public string Label() { return Tag + Describe(); }        // both reachable here
}

string BuildLabel() {
  var c = new Circle("c", 2m);
  return c.Label();                          // "c[c]"
}
```

It reaches the whole chain, not one step: a class deriving from `Circle` sees `Shape`'s protected members too.

From outside the hierarchy the member does not exist — `c.Tag` in a top-level function is a compile error naming the
fix, which is to derive from the class rather than to reach into it.

### Can a class derive from an entity?   {#kinds}
A class may derive only from a class. Mixing the kinds is refused in both directions, because they are different
things wearing one word: an [entity hierarchy](https://osysharp.com/reference/entity/inheritance/) is rows in a table with a discriminator column,
and a class is a value in memory with no table at all.

## Errors         {#errors}
| you wrote | what you get |
|---|---|
| a method already declared on the base | refused, naming `virtual`/`override` as what is missing |
| `class C : SomeSealedClass` | refused — `sealed` means no type may derive |
| `class C : SomeEntity` (or an entity deriving from a class) | refused — a class and an entity are different kinds |
| `Circle c = someShape;` | refused — the upcast is one-way; test the type instead |
| `var s = f ? circle : square;` | refused — two siblings have no common type and `var` supplies no target; declare the type, or cast one branch |
| a `List<Circle>` where a `List<Shape>` is wanted | refused — a `List` can be appended to, so it is invariant; declare `Shape[]` if it is only read |
| `shapes[0] = new Shape { … }` where `Shape` has subtypes | refused — the array may be a `Circle[]`; read it freely, or use a `List<Shape>` to write |

## See also       {#see-also}
- [Interfaces — a contract several types can satisfy](https://osysharp.com/reference/class/interfaces/) — a contract with no bodies and no state, which a type may satisfy SEVERAL of
  (a base class it may have only one of)
- [Testing which class a value is](https://osysharp.com/reference/class/type-tests/) — asking which type a value actually is, and narrowing to it
- [class methods](https://osysharp.com/reference/class/methods/) — behaviour with a receiver
- [entity Sub : Base](https://osysharp.com/reference/entity/inheritance/) — the same word for rows, and why it works differently
- [Classes](https://osysharp.com/reference/class/index/) — what a class is


---

<!-- https://osysharp.com/reference/class/index/ -->

# Classes

> A class is an in-memory shape — data plus the behaviour that belongs to it — and it never touches the database. That is the whole distinction from an entity: an entity is a table, a class is something you build, pass, and compute with in a function or a component. Classes have a constructor and methods, exactly as in C#, and like a C# class they are REFERENCE types: `var b = a;` aliases rather than copies, and `list[i].Field = x` sticks.

<!-- id: class-index · area: class · stability: stable · html: https://osysharp.com/reference/class/index/ -->

## Summary        {#summary}
A `class` is an **in-memory shape**: fields and the behaviour that belongs to them. It is not persisted and
has no table — that is the one line that separates it from an [entity](https://osysharp.com/reference/entity/declaration/). Like a C# class it is a
**reference type** ([[class-index#reference|what that means for assignment]]). Reach for a class when
you need a structured value to compute with inside a function or a component: a parsed request, a calculation's
intermediate, a projection's target, a small bundle of related fields you pass around.

If you're deciding between the two: **does it need to be stored and queried?** Yes → an entity. No → a class.

## Description    {#description}

### A value, not a row   {#vs-entity}
An entity lives in the database; the platform loads it, tracks your edits, and commits them. A class does none of
that — you `new` it, read and write its fields, hand it to another function, and it vanishes when the work is done. No
schema, no migration, no security rules: it is just a value in memory, like any C# object. It carries its own
[constructor](https://osysharp.com/reference/class/constructors/) and [methods](https://osysharp.com/reference/class/methods/):

```osy title="a value with a constructor and a method" test app=class-index
class Money {
  public decimal Amount;
  public string Currency;

  // the ctor assigns the required Currency, so `new Money(9.5m, "USD")` needs no initializer — the compiler infers it
  public Money(decimal amount, string currency) {
    Amount = amount;
    Currency = currency;
  }

  public string Label() => Amount.ToString("F2") + " " + Currency;
}
```

Build one and read its label — the constructor runs, then the method runs on the value:

```osy title="build one and read its label" run app=class-index
[Test]
void Builds_And_Labels() {
  var m = new Money(9.5m, "USD");
  Assert.Equal("9.50 USD", m.Label());
}
```

### Does `var b = a;` copy it, or point at it?   {#reference}
It points at it. **A class is a REFERENCE type, exactly like a C# `class`** — "value" above describes what a class is
*for* (a shape you compute with, with no table behind it), never how assignment behaves. Three consequences, and all
three are the C# ones:

- `var b = a;` makes `b` **another name for the same object**. A write through `b` is visible through `a`.
- `list[i].Field = x` **sticks** — the indexer hands back the object, not a copy of it, so you can edit rows in place.
- A list you filtered holds **the same objects** as the list you filtered it from, so editing through one is visible
  through the other.

⚑ **In a component, that in-place write also RE-RENDERS** — a render slot reading a class field is tracked like any
other read, so you never need to reassign the list to make the screen move. What you cannot do is point a control's
two-way `value:` at a class field. Both halves, with compiled proof: [[ui-reactivity#class-values]].

```osy title="assignment aliases, and an edit through an index sticks" test app=class-index
class Ticket {
  public string Code;
  public decimal Price;
  public Ticket(string code, decimal price) { Code = code; Price = price; }
}
```

```osy title="the three things people write around when they assume a copy" run app=class-index
[Test]
void A_Class_Is_A_Reference() {
  var a = new Ticket("T1", 10m);
  var b = a;
  b.Price = 99m;
  Assert.Equal(99m, a.Price);            // same object — `b` was never a copy

  var rows = new List<Ticket>();
  rows.Add(new Ticket("T2", 1m));
  rows.Add(new Ticket("T3", 2m));
  rows[0].Price = 42m;
  Assert.Equal(42m, rows[0].Price);      // an edit through the indexer sticks

  var dear = rows.Where(t => t.Price > 1m).ToList();
  dear[0].Price = 50m;
  Assert.Equal(50m, rows[0].Price);      // the filtered list holds the SAME objects

  var missing = rows.FirstOrDefault(t => t.Code == "nope");
  Assert.True(missing == null);          // no match is null, not an empty Ticket
}
```

⚑ [`with`](https://osysharp.com/reference/class/with/) is the one place a copy happens, and that is the point of it: `a with { Price = 5m }` builds
a **new** object and leaves `a` alone. It is opt-in copying, not evidence that assignment copies.

### It has a constructor   {#constructor}
A class declares one [constructor](https://osysharp.com/reference/class/constructors/) — its name is the class name, it takes no return type, and it
runs when you write `new T(args)`. The constructor body runs first; object-initializer syntax (`{ Member = value }`)
applies after it. Use the constructor for the setup a valid instance always needs.

### It has methods   {#methods}
Behaviour that belongs to a shape lives on the shape. A [method](https://osysharp.com/reference/class/methods/) has a receiver and is called as
`value.Method(...)` — the natural home for logic that is *about* this value rather than about the database. A method
body resolves function-style with the class as its receiver, the same mechanism a component's `action` uses.

### It has properties   {#properties}
A [property](https://osysharp.com/reference/class/properties/) reads and writes like a field but runs a body on access — a value computed from other
fields (`Total => Qty * Price`), a setter that validates a write, or an auto-property (`{ get; set; }`) whose storage
the platform synthesizes. It is a field-shaped pair of methods, so it works everywhere a method does, the browser
included.

### Its fields can have defaults   {#defaults}
A field can carry an initializer — `decimal Rate = 0.25m;`. It runs at construction, before the constructor body and
before any object initializer, exactly as in C#: the constructor (and every read) sees the declared value unless
something later overwrites it.

```osy title="a field with a default" test app=class-index
class Cart {
  public decimal Rate = 0.25m;
}
```

```osy title="the default is there before you touch it" run app=class-index
[Test]
void Field_Default_Applies() {
  var c = new Cart();
  Assert.Equal(0.25m, c.Rate);          // the declared default, applied at construction
  var d = new Cart { Rate = 0.1m };
  Assert.Equal(0.1m, d.Rate);           // an object initializer overrides it
}
```

### Visibility follows C#   {#visibility}
A top-level `class` is `internal` by default (an entity or enum is `public`) — see [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/). Mark it
`public` when code in another namespace needs to name it.

## See also       {#see-also}
- [constructor](https://osysharp.com/reference/class/constructors/) — the single constructor and how it composes with object initializers
- [Class inheritance](https://osysharp.com/reference/class/inheritance/) — deriving one class from another, and `sealed`
- [Interfaces — a contract several types can satisfy](https://osysharp.com/reference/class/interfaces/) — a contract several types satisfy, and calling through it
- [Testing which class a value is](https://osysharp.com/reference/class/type-tests/) — asking which type a value actually is
- [class methods](https://osysharp.com/reference/class/methods/) — behaviour with a receiver, called `value.Method(…)`
- [class properties](https://osysharp.com/reference/class/properties/) — members that read/write like a field but run a body on access
- [`readonly` fields](https://osysharp.com/reference/class/readonly/) — a field only the constructor may set
- [Copying a class with changes](https://osysharp.com/reference/class/with/) — copying a value and replacing some of its fields
- [Method overloads](https://osysharp.com/reference/class/overloads/) — several methods sharing a name, told apart by their parameters
- [`static` methods](https://osysharp.com/reference/class/static/) — a method that belongs to the type rather than to an instance
- [`params` parameters](https://osysharp.com/reference/class/params/) — a method callable with any number of trailing arguments
- [entity](https://osysharp.com/reference/entity/declaration/) — the persisted counterpart, when the shape needs a table
- [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/) — why a class defaults to `internal`


---

<!-- https://osysharp.com/reference/class/equality/ -->

# Comparing classes

> `==` on two class values compares them BY VALUE, field by field, rather than by reference. A class is a value you build rather than a row you hold, and the runtime rebuilds it freely — so two separately constructed values with the same contents are equal, and a class rebuilt on the next render still equals the one you kept.

<!-- id: class-equality · area: class · stability: preview · html: https://osysharp.com/reference/class/equality/ -->

## Summary        {#summary}
Two class values are equal when **their fields are equal**:

```osy title="value equality, not reference equality" test app=class-equality
class Money {
  public decimal Amount;
  public string Currency;
}

bool SamePrice() {
  var a = new Money { Amount = 9.50m, Currency = "USD" };
  var b = new Money { Amount = 9.50m, Currency = "USD" };
  return a == b;                 // true — two values, one content
}
```

`a` and `b` were built separately and are still equal. That is the rule, and it is the one place Osy# deliberately
answers differently from C#'s `class`.

## Signature      {#signature}
```osy syntax
a == b        // true when both are the SAME class and every field is equal
a != b        // the negation
a == null     // a class is a reference type, so this is a legitimate check
```

## Description    {#description}

### Why value equality   {#why}
A class is an **in-memory value**, not a row: it has no table and no id, and the platform rebuilds it wherever it
needs to. A `new` written inside a component's `render` is rebuilt on every render. A class handed between the
browser and the server is serialised and reconstructed on the far side. Nothing preserves object identity across any
of that.

So reference equality would not merely be a different answer — it would be an answer that can never be `true` for two
values you did not personally hold onto within a single expression. That is why the rule is value equality: it is the
only question the runtime can answer honestly, and it is the one authors are asking.

If you are coming from C#: an Osy# `class` behaves like a C# **`record`**, not like a C# `class`.

### What "equal fields" means   {#field-comparison}
Field comparison is the ordinary `==` applied to each field, so the rule composes:

| a field holding | compares by |
|---|---|
| a scalar (`int`, `string`, `decimal`, `DateTime`, an enum) | its value |
| another class | its fields, recursively |
| an entity | that entity's identity, exactly as `==` on an entity does |
| a `Func<…>` / `Action<…>` | the lambda it came from — see below |
| nothing (never assigned) | `null`, which is what an unset field is |

Two values of **different** classes are never equal, however identical their fields.

### A field holding a function   {#function-fields}
A selector field compares by **which lambda it is**, not by the object wrapping it:

```osy title="a shape carrying a selector" test app=class-equality-func
class Column<T> {
  public string Label;
  public Func<T, string> Value;
}
```

Two `Column` values written from the same `r => r.Title` are equal; two written from different lambdas are not. This
is the same question C# asks of a method group, and it is what lets a shape that carries behaviour — a table column,
a formatting rule — be compared at all. Without it a single function field would make every such value unequal to
every other, including to itself one render later.

### Entities compare differently, and should   {#entities}
An [entity](https://osysharp.com/reference/entity/declaration/) is a row, so two entity values are equal when they are **the same row** — its
identity, not its current field values. Two rows with identical columns are still two rows. A class has no identity
to compare, which is exactly why its contents are the answer. [Comparing entity rows](https://osysharp.com/reference/entity/equality/) has that rule in full, and what it
means for `Contains`/`IndexOf`/`Remove` over a list of rows.

## Examples       {#examples}

```osy title="a rebuilt value still matches the one you kept" test app=class-equality-render
class Column<T> {
  public string Name;
  public Func<T, string> Value;
}

[Composable] component SortHeader<T>(Column<T>[] columns) {
  Column<T> sortBy = columns[0];
  bool descending = false;

  // `columns` is rebuilt every render, so `c` is not the same OBJECT as `sortBy` — it is the same VALUE.
  // Clicking the sorted column toggles direction; clicking another one selects it.
  action Choose(Column<T> c) {
    if (sortBy == c) { descending = !descending; } else { sortBy = c; descending = false; }
  }

  render {
    Row {
      foreach (var c in columns) {
        Pressable(onClick: () => Choose(c)) { Text(c.Name); }
      }
    }
  }
}
```

## Errors         {#errors}

| Message | Cause | Fix |
|---|---|---|
| `cannot compare 'X' with 'Y'` | The two operands are unrelated types. | Compare values of the same class, or compare a field of each. |

## See also       {#see-also}
- [Classes](https://osysharp.com/reference/class/index/) — what a class is, and when to reach for one instead of an entity
- [class properties](https://osysharp.com/reference/class/properties/) — declaring the fields this rule compares
- [Copying a class with changes](https://osysharp.com/reference/class/with/) — copying a value with some fields replaced, which this rule is what makes coherent
- [Generic classes](https://osysharp.com/reference/class/generics/) — the `Column<T>` shape used above
- [Comparing entity rows](https://osysharp.com/reference/entity/equality/) — the other half of this rule: why an entity compares by identity instead
- [entity](https://osysharp.com/reference/entity/declaration/) — what a row is, and how you read one back


---

<!-- https://osysharp.com/reference/class/with/ -->

# Copying a class with changes

> `with` makes a COPY of a class value and replaces the fields you name. Everything you do not name comes from the value you copied, and the original is left untouched — so it is how you derive one value from another without rebuilding it field by field, and without mutating something another part of the program is still holding.

<!-- id: class-with · area: class · stability: preview · html: https://osysharp.com/reference/class/with/ -->

## Summary        {#summary}
`with` copies a class value and replaces the fields you name:

```osy title="a copy with one field changed" test app=class-with
class Money {
  public decimal Amount;
  public string Currency = "USD";
  public string Note = "";
}

string Rename() {
  var fee = new Money { Amount = 25m, Currency = "SEK", Note = "late fee" };
  var refund = fee with { Amount = 0m };
  return refund.Currency;        // "SEK" — carried over, not re-defaulted
}
```

`refund` is a **new value**. It took its `Amount` from the initializer and everything else — `Currency`, `Note` —
from `fee`, which is unchanged.

## Signature      {#signature}
```osy syntax
value with { Field = expr, … }   // a copy of `value`, with those fields replaced
value with { }                   // a plain copy
a with { X = 1m } with { Y = 2m }  // chains; each copy is made from the one before
```

`with` is available on any [class](https://osysharp.com/reference/class/index/). It is not available on an `entity`, or on a scalar — see
[[#errors|Errors]].

## Description    {#description}

### The fields you do not name come from the value you copied   {#copied-fields}
This is the whole point, and it is the part worth testing when something looks wrong. A `with` does **not** start
from the class's declared defaults and it does not re-run field initializers:

```osy title="declared defaults do not come back" test app=class-with-defaults
class Settings {
  public string Theme = "light";
  public decimal Zoom = 1m;
}

string Keep() {
  var dark = new Settings { Theme = "dark", Zoom = 2m };
  var zoomed = dark with { Zoom = 3m };
  return zoomed.Theme;           // "dark" — the copy's value, NOT the declared "light"
}
```

### The original is never modified   {#immutable}
`with` produces a value; it does not write through to the one it copied. Anything else still holding the original
sees exactly what it saw before:

```osy title="the donor is untouched" test app=class-with-donor
class Money { public decimal Amount; public string Note = ""; }

decimal Both() {
  var original = new Money { Amount = 10m, Note = "invoice" };
  var adjusted = original with { Amount = 25m };
  return original.Amount + adjusted.Amount;    // 35 — 10 and 25, two values
}
```

### A copy keeps the type it actually is   {#runtime-type}
If you copy a value held in a base-typed variable, the copy is the type the **value** is, not the type the variable
is declared as:

```osy title="a Dog held as an Animal copies to a Dog" test app=class-with-subtype
class Animal { public string Name = "?"; }
class Dog : Animal { public decimal Legs = 4m; }

bool StillADog() {
  Animal held = new Dog { Name = "rex", Legs = 3m };
  var renamed = held with { Name = "fido" };
  return renamed is Dog;         // true, and its Legs is still 3
}
```

### Why there is no `record` keyword   {#no-record}
In C#, `with` works on a `record` and not on a plain `class`, because the two differ in how they compare. In Osy#
they do not: a class already compares [by value](https://osysharp.com/reference/class/equality/), which is what a C# `record` is. A second keyword
would therefore separate nothing, so `with` is simply available on every class.

A consequence worth knowing: a plain copy **equals** the value it came from.

```osy title="a copy with nothing replaced is equal" test app=class-with-equality
class Point { public decimal X; public decimal Y; }

bool SameValue() {
  var p = new Point { X = 1m, Y = 2m };
  return p == p with { };        // true — same class, same fields
}
```

### `readonly` fields still cannot be set   {#readonly}
A [`readonly`](https://osysharp.com/reference/class/readonly/) field is one only the constructor may assign, and a `with` is not a constructor — it
copies an already-built value and then writes over it. To vary a `readonly` field, construct the value instead.

## Examples       {#examples}

```osy title="deriving a series of values from one" test app=class-with-series
class Quote {
  public string Customer;
  public decimal Net;
  public decimal Vat = 0m;
  public string Status = "draft";
}

decimal Finalize() {
  var draft = new Quote { Customer = "Acme", Net = 100m };
  var taxed = draft with { Vat = 25m };
  var sent  = taxed with { Status = "sent" };
  // Customer and Net rode through both copies; nobody had to restate them.
  return sent.Net + sent.Vat;    // 125
}
```

## Errors         {#errors}

| Message | Cause | Fix |
|---|---|---|
| ``with` copies an object, and a `decimal` is not one` | The value on the left is a scalar, which has no fields to replace. | Use ordinary arithmetic or assignment; `with` applies to a class. |
| ``with` copies an in-memory `class`, and 'X' is an `entity`` | The value on the left is a stored row. Copying one would silently mean either a second row or a duplicate of this one. | Create the row you want (`new X { … }`), or change the fields on the row you have. |
| `entity 'X' has no property 'Y'` | The initializer names a field the class does not declare. | Check the field name against the declaration. |
| `'X.Y' is readonly` | A `readonly` field cannot be set by a copy. | Pass the value to the constructor instead. |

## See also       {#see-also}
- [Classes](https://osysharp.com/reference/class/index/) — what a class is, and when to reach for one instead of an entity
- [Comparing classes](https://osysharp.com/reference/class/equality/) — why a class compares by value, which is why `with` needs no `record` keyword
- [`readonly` fields](https://osysharp.com/reference/class/readonly/) — the one kind of field a copy may not replace
- [constructor](https://osysharp.com/reference/class/constructors/) — building a value from scratch rather than from another one
- [entity](https://osysharp.com/reference/entity/declaration/) — why a stored row is copied differently


---

<!-- https://osysharp.com/reference/class/generics/ -->

# Generic classes

> A class can declare type parameters, so one shape serves every type it is used with instead of being copied per entity. The type argument is written where the class is used (`Column<Report>`), and every member read off it substitutes — which is what binds a selector's parameter to the real row type and makes its body checkable. A METHOD can declare its own type parameters too, and those are bound by the call rather than by the receiver.

<!-- id: class-generics · area: class · stability: preview · html: https://osysharp.com/reference/class/generics/ -->

## Summary        {#summary}
A class that describes **how to work with a value** should not have to know **which** value. Declare it with a type
parameter and one declaration serves every type:

```osy title="one column shape, any row type" test app=class-generics
class Column<T> {
  public string Label;
  public Func<T, string> Value;
}
```

Without generics that shape has to be copied once per entity — a `ReportColumn`, a `PersonColumn` — each identical
except for one type. With them, `Column<Report>` and `Column<Person>` are two **types** from one **declaration**.

## Signature      {#signature}
```osy title="declaring the parameter, supplying the argument" syntax
class Name<T> { … }            // one type parameter
class Name<TIn, TOut> { … }    // several, comma-separated

Name<Report>                   // a CONSTRUCTED type: the argument supplied at the use site
new Name<Report> { … }         // …and at construction
```

A type parameter is in scope over the whole class body — including in its methods' signatures. A **method** may also
declare parameters of its own, which the call binds rather than the receiver:

```osy title="a method's own parameter, bound by the call" syntax
class Util { public T Echo<T>(T v) { … } }    // the method's own parameter
T Echo<T>(T v) { … }                          // a top-level function, the same way
```

`entity` cannot be generic.

## Description    {#description}

### The type argument goes where the class is USED   {#use-site}
The declaration names the parameter; the use site supplies the argument. Between them, the compiler knows what `T`
is at every point:

```osy title="the argument is written at the use site" test app=class-generics
entity Report { [MaxLength(80)] string Title; decimal Total; }

[Composable] component Grid<T>(T[] rows, Column<T>[] columns) {
  render {
    foreach (var r in rows) {
      Row { foreach (var c in columns) { Text(c.Value(r)); } }
    }
  }
}

[Page("/")] [AllowAnonymous]
component Home() {
  var reports = Report.ToList();          // the read is a MEMBER — a render body holds values, it does not fetch
  render {
    Grid(rows: reports, columns: [
      new Column<Report> { Label = "Title", Value = r => r.Title },
      new Column<Report> { Label = "Band",  Value = r => r.Total > 1000 ? "large" : "small" }
    ]);
  }
}
```

`Grid` mentions no entity at all. `Column<T>` mentions no entity. The only place `Report` appears is the call site —
which is exactly the point.

### Reading a member substitutes the argument   {#substitution}
`Value` is declared `Func<T, string>`. Read off a `Column<Report>` it **is** a `Func<Report, string>`, so the lambda's
parameter binds to a real row and its body is checked against it:

```osy title="the selector's parameter is the row type — and typos are caught" syntax
new Column<Report> { Value = r => r.Title }    // ✓ r is a Report
new Column<Report> { Value = r => r.Nmae }     // ✗ 'Report' has no property 'Nmae'
```

That substitution is the whole feature. Passing the wrong one is caught too — a `Column<Person>` where a
`Column<Report>` is expected is a different type, not merely a differently-labelled `Column`.

### Where a type parameter may appear   {#positions}
`T` is first-class **on its own**, as an **array**, and inside a **function type**:

```osy title="the shapes a member may take" test app=class-generics
class Holder<T> {
  public T Row;                      // the type parameter itself
  public T[] Rows;                   // an array of it
  public Func<T, string> Read;       // a selector over it
}
```

What is not built yet is `T` inside a **mutable collection or another generic type** — `List<T>`, `HashSet<T>`,
`Dictionary<string, T>`. Those carry a collection kind and a value slot that the compiler derives from the
declaration alone, so they need their own storage rather than being let through. The refusal names the three
spellings that do work.

An unset member typed `T` reads `default(T)`, decided by the type argument: a `Holder<decimal>` reads `0m`, a
`Holder<Report>` reads null — because null *is* `default` for a type with no meaningful zero.

### Methods can have their OWN type parameters   {#method-generics}
A method may declare type parameters that the class does not, and they are bound by the **call** rather than by the
receiver — inferred from the argument types:

```osy title="one method, a different type at every call" test app=class-generic-methods
class Util {
  public T Echo<T>(T v) { return v; }
  public string Describe<T>(T v) { return "described"; }
}

string UseIt() {
  var u = new Util();
  var n = u.Echo(2m);                // T is decimal here
  var s = u.Echo("hello");           // …and string here, from the same declaration
  return s;
}
```

This is the difference worth holding on to: a generic **class** fixes its argument once, where it is used
(`Column<Report>`), and every member read off it substitutes that one answer. A generic **method** decides per call.

Write the arguments explicitly when inference has nothing to read them from — a type parameter that appears in no
parameter cannot be inferred, and the compiler says so rather than guessing:

```osy title="explicit type arguments" test app=class-generic-methods
string Explicitly() {
  var u = new Util();
  return u.Echo<string>("hello");
}
```

A **top-level function** may be generic the same way, and so may a method on a class that is itself generic — there
the two sets are bound by different things, the class's by the receiver and the method's by the call. A method may
**not** reuse one of its class's parameter names: the two would be different types wearing one name, and nothing in
the source would show the reader which is which.

### Classes only   {#no-generic-entity}
An `entity` cannot be generic. An entity is a table, and a table has no columns until `T` is known:

```osy title="a table cannot be generic" syntax
entity Row<T> { string Label; }      // ✗ — write `class Row<T>` for an in-memory shape
```

## Examples       {#examples}

A comparator and a formatter are the same shape as a column, which is why this generalises past grids:

```osy title="the same idea, two other uses" test app=class-generics-more
class Sorter<T> {
  public string Label;
  public Func<T, string> Key;
}

class Formatter<T> {
  public Func<T, string> Render;
}
```

Several type parameters are written the way C# writes them:

```osy title="more than one parameter" test app=class-generics-more
class Mapping<TIn, TOut> {
  public Func<TIn, string> Read;
  public Func<TOut, string> Write;
}
```

### Deriving from a generic {#deriving}

A base or a contract may be **constructed** — `: Box<int>`, `: IRepo<string>`. The members that arrive are
substituted, so the subtype holds the closed type and not the parameter:

```osy title="closing a generic base" test app=class-generics-derive
class Box<T> {
  public T Value;
  public T Get() { return Value; }
}

// `Value` is an `int` on IntBox, so ordinary arithmetic works on it.
class IntBox : Box<int> {
  public int Doubled() { return Value * 2; }
}
```

The subtype may stay generic and pass its own parameter on, and a third type close it. Substitution **composes**
through the chain, so `Leaf.Value` is a `string`:

```osy title="passing a parameter on" test app=class-generics-derive
class Mid<U> : Box<U> { public string Tag = "mid"; }
class Leaf : Mid<string> { }
```

A contract works the same way, and an implementor satisfies it **at the arguments it named** — a `TextRepo` is an
`IRepo<string>` and is not an `IRepo<int>`:

```osy title="a generic contract" test app=class-generics-derive
interface IRepo<T> { T Fetch(); void Store(T v); }

class TextRepo : IRepo<string> {
  public string Held = "";
  public string Fetch() { return Held; }
  public void Store(string v) { Held = v; }
}
```

A `where` on a subtype goes after the base list, exactly as in C#:

```osy title="passing a constraint on" test app=class-generics-derive
class Shape { public string Kind = "shape"; }
class Holder<T> where T : Shape { public T Held; }
class ShapeHolder<T> : Holder<T> where T : Shape { }
```

## Errors         {#errors}

| What you wrote | What you get |
|---|---|
| `entity Row<T> { … }` | *'Row' is an `entity`, and an entity cannot be generic — it is a table, and a table has no columns until its type argument is known.* |
| `public List<T> Rows;` on a `class Holder<T>` | *'Holder.Rows' is typed `List<T>`, and a type parameter is not supported inside a mutable collection or a generic type yet — it works on its own (`T Rows;`), as an ARRAY (`T[] Rows;`), or inside a function type (`Func<T, string> Rows;`).* |
| `Column<Report, Report>` | *'Column' takes 1 type argument (T), but 2 were written.* |
| `u.Echo<decimal, string>(2m)` on `T Echo<T>(T v)` | *'Echo' declares 1 type parameter ('T'), but 2 were given.* |
| calling `string Make<T>()`, which mentions `T` in no parameter | *'Make' cannot infer its type parameter 'T' from these arguments … Write the type argument explicitly: `Make<T>(…)`.* |
| `public string Show<T>(T v)` inside a `class Box<T>` | *'Show' declares a type parameter 'T', and so does the type that declares it … Rename one of them.* |
| `c.Add<decimal>(1m)` where `Add` declares no type parameters | *'Add' declares no type parameters, so the type argument in `Add<…>(…)` binds to nothing … Drop the type arguments.* |
| `Column` with no argument | *'Column' is generic — it needs a type argument for 'T'.* |
| a `Column<Person>` where a `Column<Report>` is expected | *expects `Column<T>[]` for 'columns', but got `Column<Person>[]`.* |
| `class Bad : Box<int, string>` on a `class Box<T>` | *'Box' takes 1 type argument (T), but 2 were written on `Bad`.* |
| `class Bad2 : NotGeneric<int>` | *'NotGeneric' is not generic, so `Bad2 : NotGeneric<int>` has nowhere to put those arguments.* |
| `class BadBox : Box<int>` on a `class Box<T> where T : Shape` | *'Box' constrains 'T' to 'Shape', and 'int' is not a class at all — a bound names a type to derive from.* |
| `IRepo<string> r = new IntRepo();` where `IntRepo : IRepo<int>` | *cannot implicitly convert 'IntRepo' to 'IRepo<string>'.* |

## See also       {#see-also}
- [Func<T, R>](https://osysharp.com/reference/ui/function-value/) — `Func<T, R>` as a value: what a selector IS, and why its body reads only its parameters
- [Classes](https://osysharp.com/reference/class/index/) — the class itself: a value shape, never a table
- [class properties](https://osysharp.com/reference/class/properties/) — members that run a body on access
- [component](https://osysharp.com/reference/ui/component/) — generic components, which take their type argument by inference from the call site


---

<!-- https://osysharp.com/reference/class/interfaces/ -->

# Interfaces — a contract several types can satisfy

> An `interface` declares what a type must do without saying how — methods and property contracts, no bodies and no state. A class may implement several of them alongside its base class, a value typed as the interface accepts any implementor, and the call runs the implementation the value actually holds.

<!-- id: class-interfaces · area: class · stability: stable · html: https://osysharp.com/reference/class/interfaces/ -->

## Summary        {#summary}
An **`interface`** is a contract: what a type must provide, with no bodies and no state.

```osy title="a contract and two implementations" test app=class-interfaces
interface IBlobStore {
  string Put(string key, string body);
  string Name { get; }
}

class S3Store : IBlobStore {
  public string Put(string key, string body) { return "s3://" + key; }
  public string Name { get; } = "s3";
}

class GcsStore : IBlobStore {
  public string Put(string key, string body) { return "gs://" + key; }
  public string Name { get; } = "gcs";
}
```

A value typed as the interface accepts either, and the call runs whichever it holds:

```osy title="the call follows the value, not the slot" test app=class-interfaces
string Upload(IBlobStore store, string key) {
  return store.Put(key, "…");        // "s3://k" or "gs://k" — decided by what `store` IS
}
```

## Signature      {#signature}
```osy syntax
interface IName {
  ReturnType Method(params);         // an obligation to DO something
  Type Property { get; }             // an obligation to ANSWER something
  Type Property { get; set; }        // …and to accept one
}

class Name : Base, IOne, ITwo { … }  // one base class, then any number of contracts
interface IBoth : IOne, ITwo { }     // an interface may extend others
```

Members are **public** without saying so, and **abstract** without saying so — that is what an interface is.

## Description    {#description}

### Interface or abstract class?    {#which}
Both let you call through a shape rather than a concrete type. Choose by what you need to share:

| | `interface` | `abstract class` |
|---|---|---|
| shared **behaviour** (a body) | no | yes |
| shared **state** (a field) | no | yes |
| how many a type may have | **many** | one |

An interface is the right default for "these types all do X" — a store, a formatter, a notifier. Reach for an
[abstract class](https://osysharp.com/reference/class/inheritance/) when implementors should share code, not just a shape.

### Implementing one       {#implementing}
A type lists its contracts after its base, and must provide **every** member — the compiler names the ones it is
missing. An inherited implementation counts:

```osy title="the base already answers the contract" test app=class-interfaces
class Logged : S3Store { }           // Put/Name come from S3Store, so `Logged` is an IBlobStore too
```

An `abstract class` may implement a contract **partially** and leave the rest to its subclasses.

### Holding and testing one    {#using}
An interface is an ordinary type: a field, a parameter, a return, a collection element. `is` narrows back out of it:

```osy title="a list of contracts, and narrowing out" test app=class-interfaces
string Describe(List<IBlobStore> stores) {
  var s = "";
  foreach (var store in stores) {
    s = s + store.Name;
    if (store is GcsStore) { s = s + "(google)"; }
  }
  return s;
}
```

### What an interface may not do   {#refusals}
Each of these is refused with the reason, not a parse error:

- **It cannot be created.** `new IBlobStore()` has no body to run — create one of the implementors.
- **It holds no state.** A field is refused; a property contract (`int Count { get; }`) is how you require a value.
- **Its members take no body.** Behaviour shared between implementors belongs on an abstract class.
- **An `entity` implements none.** A row lives in one table told apart by one discriminator; a contract is a
  compile-time promise with nothing to store. Move the behaviour to a `class`.

⚠ **A generic contract cannot be inherited yet** — `class R : IRepo<int>` is refused, and so is `class B : Box<int>`.
That is the generic-inheritance limit, not an interface one; the compiler says so where you write it.

## Examples       {#examples}
Choosing an implementation at run time — the shape this exists for:

```osy title="pick a store, then use it through the contract" test app=class-interfaces
string Store(bool useGcs, string key) {
  IBlobStore store = new S3Store();
  if (useGcs) { store = new GcsStore(); }
  return store.Name + " " + store.Put(key, "payload");
}
```

Two contracts on one type:

```osy title="a type may promise several things" test app=class-interfaces
interface IAudited { string Who(); }

class AuditedS3 : S3Store, IAudited {
  public string Who() { return "auditor"; }
}

string Trace(IAudited a) { return a.Who(); }
```

## See also       {#see-also}
- [Class inheritance](https://osysharp.com/reference/class/inheritance/) — `class B : A`, `virtual`/`override`/`abstract`, and when a base class is the better shape
- [class methods](https://osysharp.com/reference/class/methods/) — the members an implementation is made of
- [Sequence fields on a class](https://osysharp.com/reference/class/collections/) — holding many implementors in a `List<IContract>`


---

<!-- https://osysharp.com/reference/class/overloads/ -->

# Method overloads

> A class can declare several methods with the same name, as long as they differ in their parameter types. Each call picks the one that fits its arguments — by how many, then by their types, then by which parameter type is most specific. A different return type or different parameter names is not a difference, and two methods that differ only that way are a compile error.

<!-- id: class-overloads · area: class · stability: stable · html: https://osysharp.com/reference/class/overloads/ -->

## Summary        {#summary}
Two methods, one name, different parameter types:

```osy title="an overload set" test app=class-overloads
class Calc {
  public decimal Add(decimal a) {
    return a;
  }

  public decimal Add(decimal a, decimal b) {
    return a + b;
  }
}
```

Each call site picks the one its arguments fit:

```osy title="each call runs its own body" run app=class-overloads
[Test]
void Each_Call_Picks_Its_Overload() {
  var c = new Calc();
  Assert.Equal(1m, c.Add(1m));
  Assert.Equal(3m, c.Add(1m, 2m));
}
```

## Signature      {#signature}
```osy syntax
class C {
  T M(A a) { … }          // one overload
  T M(A a, B b) { … }     // another — different parameter COUNT
  T M(B b) { … }          // another — different parameter TYPE
}
```

## Description    {#description}

### What makes two overloads different?   {#what-differs}
**Their parameter types, in order — and nothing else.** Two methods that differ only in return type, or only in
parameter names, are the same method declared twice, and that is a compile error:

```osy syntax
decimal Add(decimal a) { … }
string  Add(decimal b) { … }   // ✗ same signature — the return type and the name `b` are not differences
```

The reason is that a call site cannot act on either one. `c.Add(1m)` says nothing about what it wants back, and it
does not name the parameter, so there would be no way to say which you meant.

### Which overload does a call pick?   {#how-a-call-picks}
In three steps, stopping as soon as one candidate is left.

**1. How many arguments.** Only the overloads that your arguments can bind to survive — counting
[default values](https://osysharp.com/reference/class/methods/), which make a parameter optional, and named arguments, which bind by name rather
than position. This is usually the whole story:

```osy title="chosen by argument count" run app=class-overloads
[Test]
void Arity_Decides() {
  var c = new Calc();
  Assert.Equal(5m, c.Add(5m));        // the one-parameter Add
  Assert.Equal(9m, c.Add(4m, 5m));    // the two-parameter Add
}
```

**2. What type they are.** When several overloads take the right number of arguments, the ones whose parameters
your arguments actually fit survive:

```osy title="chosen by argument type" test app=class-overloads
class Formatter {
  public string Show(decimal d) { return "number"; }
  public string Show(string s) { return "text"; }
}
```

```osy title="the type of the argument decides" run app=class-overloads
[Test]
void Type_Decides() {
  var f = new Formatter();
  Assert.Equal("number", f.Show(1m));
  Assert.Equal("text", f.Show("x"));
}
```

**3. Which is most specific.** If more than one still fits, the one whose parameter type is *lower in the class
hierarchy* wins — `Circle` beats `Shape`:

```osy title="the more specific overload wins" test app=class-overloads
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

class Painter {
  public string Draw(Shape s) { return "shape"; }
  public string Draw(Circle c) { return "circle"; }
}
```

```osy title="a Circle picks Draw(Circle)" run app=class-overloads
[Test]
void Most_Specific_Wins() {
  var p = new Painter();
  Assert.Equal("circle", p.Draw(new Circle { Name = "a", Radius = 1m }));
}
```

### It is the DECLARED type that chooses, not the runtime one   {#static-not-virtual}
This is the one rule worth reading twice, because it differs from how [`override`](https://osysharp.com/reference/class/inheritance/) works.
**Which overload runs is decided when your code is compiled, from the type of the variable you are holding.** Which
`override` runs is decided while it runs, from the type of the object.

```osy title="the slot decides, not the value" run app=class-overloads
[Test]
void The_Declared_Type_Chooses() {
  var p = new Painter();
  Shape held = new Circle { Name = "a", Radius = 1m };
  Assert.Equal("shape", p.Draw(held));    // held is declared `Shape` — even though it holds a Circle
}
```

If you want the object to decide, that is what `virtual`/`override` is for — see [Class inheritance](https://osysharp.com/reference/class/inheritance/).

### Can a subclass add an overload?   {#inheritance}
A subclass inherits its base's overloads and can add to the set. Declaring a method with a **new** signature adds an
overload; declaring one with an **existing** signature overrides it (and must say `override`):

```osy title="one adds, one overrides" test app=class-overloads
class Reporter {
  public virtual string Line(decimal a) { return "base-1"; }
}

class RichReporter : Reporter {
  public override string Line(decimal a) { return "rich-1"; }      // same signature → an override
  public string Line(decimal a, decimal b) { return "rich-2"; }    // new signature → a sibling overload
}
```

```osy title="both are callable, and the override still dispatches" run app=class-overloads
[Test]
void Adding_And_Overriding() {
  var r = new RichReporter();
  Assert.Equal("rich-1", r.Line(1m));
  Assert.Equal("rich-2", r.Line(1m, 2m));

  Reporter asBase = r;
  Assert.Equal("rich-1", asBase.Line(1m));    // virtual dispatch still finds the override
}
```

`base.Line(…)` picks from the base's set the same way, so an override of one overload can still call either.

### When a call is ambiguous   {#ambiguous}
If two overloads both fit and neither is more specific, the call is refused rather than guessed. Say which you mean
by giving the argument a declared type:

```osy syntax
class P {
  string Go(Left? l, Right? r) { … }
  string Go(Right? r, Left? l) { … }
}

p.Go(null, null);        // ✗ ambiguous — both fit, neither is more specific
Left? l = null;
p.Go(l, null);           // ✓ the declared type of `l` settles it
```

### A method and a property cannot share a name   {#properties}
Only methods overload. A property has no overload set to join, so a method named after a property is refused — one
of the two has to be renamed.

### Constructors do not overload yet   {#constructors}
A class declares one [constructor](https://osysharp.com/reference/class/constructors/). Give it the widest parameter list and default the ones a
caller may omit:

```osy syntax
public Box(decimal width, decimal height = 1m) { … }   // one constructor, two ways to call it
```

## See also       {#see-also}
- [class methods](https://osysharp.com/reference/class/methods/) — declaring methods, default values, and named arguments
- [Class inheritance](https://osysharp.com/reference/class/inheritance/) — `virtual`/`override`, and why they decide at a different moment than overloads do
- [constructor](https://osysharp.com/reference/class/constructors/) — the single constructor, and defaulting its parameters
- [Classes](https://osysharp.com/reference/class/index/) — what a class is


---

<!-- https://osysharp.com/reference/class/collections/ -->

# Sequence fields on a class

> A class can hold many values in one field — `string[]`, `int[]`, `List<Tag>`, `HashSet<string>`, `Dictionary<string, int>`. The element may be a scalar, an enum, or another class; the whole LINQ surface reads it, a `foreach` over a map binds each entry's `Key` and `Value`, and an array is fixed-size exactly as in C#. An entity cannot hold one: a collection member of an entity is a relation.

<!-- id: class-collections · area: class · stability: stable · html: https://osysharp.com/reference/class/collections/ -->

## Summary        {#summary}
A `class` field can hold **many values**. Four spellings, all of them C#'s:

```osy title="four ways to hold many values" test app=class-collections
class Tag { public string Name; }

class Document {
  public string Title;
  public string[] Keywords;             // fixed-size — its length is part of it
  public int[] Scores;                  // any scalar, not just string
  public List<Tag> Tags;                // growable
  public HashSet<string> Seen;          // distinct, unordered
  public Dictionary<string, int> Counts;
}
```

## Signature      {#signature}
```osy syntax
public T[] Field;                      // array   — fixed-size; indexable, .Count, foreach, LINQ. No .Add
public List<T> Field;                  // list    — everything an array does, plus .Add / .Remove / .RemoveAll
public HashSet<T> Field;               // set     — distinct membership; .Add / .Contains / .Remove
public Dictionary<K, V> Field;         // map     — d[k], .ContainsKey, .TryGetValue, .GetValueOrDefault, .Keys, .Values
```

`T` may be a **scalar** (`string`, `int`, `long`, `decimal`, `double`, `bool`, `Guid`, `DateTime`, …), an **enum**
you declared, **another class**, or **another collection** — `Dictionary<string, List<int>>` and `List<List<int>>`
are both fine, at any depth. `byte[]` is the exception and is not a sequence at all — it is the binary scalar type.

⚠ The one thing that cannot nest is a **Dictionary KEY**: a key has to be comparable, and a collection is not. The
compiler says so rather than storing it and failing later.

## Description    {#description}

### Reading one   {#reading}
Every sequence field answers the same questions, and they are the [LINQ verbs](https://osysharp.com/reference/query/in-memory-linq/) you already
use — filter, project, sort, aggregate — evaluated in memory over the values the object holds:

```osy title="the whole LINQ surface, over a field" test app=class-collections
int LongKeywords(Document d) {
  return d.Keywords.Where(k => k.Length > 5).Count;
}

string FirstAlphabetically(Document d) {
  return d.Keywords.OrderBy(k => k).First(k => k != "");
}

bool EveryScorePasses(Document d) {
  return d.Scores.All(s => s >= 0);
}

// `Max` has no zero to fall back on, so over NO scores it answers absent — which is why this returns `int?`
// where `Count` above returns `int`. Writing `int` here is a compile error, not a silent 0.
int? Best(Document d) {
  return d.Scores.Max(s => s);
}
```

Indexing and `foreach` read it directly, with no ceremony:

```osy title="index it, walk it" test app=class-collections
int Total(Document d) {
  var sum = 0;
  foreach (var s in d.Scores) { sum = sum + s; }
  return sum + d.Scores[0];
}
```

### A collection member starts EMPTY   {#starts-empty}
A sequence member of a class is an empty collection from the moment the object is made — you never have to check
it before adding to it:

```osy syntax
class Valley {
  List<Prop> trees = new List<Prop>();

  public void Build() {
    trees.Add(new Prop { X = 1 });     // works on a brand-new Valley
  }
}
```

The initializer above is the natural way to write it and is what most people will. It is not load-bearing: a
`List<Prop> trees;` with no initializer is empty too. Every other reading in the language already treats an absent
collection as empty — iterating one yields nothing, `.Count` answers 0, a query over it is empty — so `.Add` agrees
with them rather than being the one operation that does not.

### Filling one   {#filling}
A collection literal fills any of them at construction:

```osy title="build one" test app=class-collections
Document New() {
  return new Document {
    Title = "notes",
    Keywords = ["osy", "reference"],
    Scores = [10, 30, 20]
  };
}
```

### Reading a map         {#dictionary}
`d[k]` reads one entry and **throws when the key is absent**, exactly as in C#. The two total forms are the ones to
reach for when a miss is ordinary:

```osy title="reading a key that may not be there" test app=class-collections
// `.TryGetValue` — C#'s own spelling, and the value is in scope in the branch.
int SeenOrZero(Document d, string word) {
  if (d.Counts.TryGetValue(word, out var n)) { return n; }
  return 0;
}

// `.GetValueOrDefault` — the same question as an expression, so it composes with `??`.
int Seen(Document d, string word) {
  return d.Counts.GetValueOrDefault(word, 0);
}
```

⚠ With **no fallback**, `.GetValueOrDefault(k)` answers **null** on a miss rather than the type's zero. That is a
deliberate difference from C#, and it is the useful one: a `0` is indistinguishable from a *stored* `0`, so a miss
would be invisible. A null makes you decide.

### Iterating a map       {#iterating}
**A `foreach` over a map binds each entry as a pair, with `Key` and `Value`** — C#'s own shape:

```osy title="every entry, both halves" test app=class-collections
string Summarise(Document d) {
  var s = "";
  foreach (var kv in d.Counts) { s = s + kv.Key + "=" + kv.Value + ";"; }
  return s;
}
```

The same pair is what the **LINQ verbs** filter and project over, so a map is a source like any other sequence:

```osy title="LINQ straight over a map" test app=class-collections
int TotalRepeated(Document d) {
  return d.Counts.Where(kv => kv.Value > 1).Sum(kv => kv.Value);
}

// `First` answers the PAIR, so its `Key` is one hop away.
string Rarest(Document d) {
  return d.Counts.OrderBy(kv => kv.Value).First(kv => kv.Value > 0).Key;
}
```

`.ToList()` and `.ToArray()` both hand you the pairs as a plain sequence.

`.Keys` and `.Values` are still there when you only want one half:

```osy title="one half at a time" test app=class-collections
int TotalSeen(Document d) {
  return d.Counts.Values.Sum();
}

int HowManyKeys(Document d) {
  return d.Counts.Keys.Count;
}
```

Both are a **snapshot**, not C#'s live view: a `.Values` you already took does not follow a later `.Add`. Take it
again when you want the current contents.

### Changing a map        {#map-changing}
`.Add(k, v)` **throws if the key is taken** — that is exactly what separates it from `d[k] = v`, which overwrites.
When you mean "add only if absent", `.TryAdd` says so and answers whether it went in:

```osy title="the three ways to write into a map" test app=class-collections
void Record(Document d, string word) {
  d.Counts[word] = 1;                    // upsert — replaces whatever was there
  d.Counts.TryAdd(word, 1);              // adds only if absent; answers false if not
  d.Counts.Remove(word);                 // answers whether it was there
  d.Counts.Clear();                      // empties it
}

bool AnyoneAt(Document d, int n) {
  return d.Counts.ContainsValue(n);      // the mirror of .ContainsKey
}
```

### Changing one          {#changing}
A `List<T>` grows and shrinks with `.Add(x)` and `.Remove(x)`. **`.RemoveAll(x => …)` deletes every match in one go
and answers how many went** — it changes the list you called it on, so anything else holding that same list sees the
change too:

```osy title="remove every match, in place" test app=class-collections
int DropShortTags(Document d) {
  return d.Tags.RemoveAll(t => t.Name.Length < 3);
}
```

That in-place mutation is the difference from `d.Tags.Where(…)`, which answers a **new** sequence and leaves the
original alone. Reach for `Where` when you want a filtered view, and `RemoveAll` when the list itself should change.

The rest of C#'s vocabulary is there too — `.AddRange(other)` appends every item of another collection,
`.Insert(i, x)` places one at a position, and `.Clear()` empties it (on a `List`, a `HashSet` or a `Dictionary`):

```osy title="append, place, empty" test app=class-collections
void Reset(Document d, List<Tag> extra) {
  d.Tags.AddRange(extra);          // append all of them
  d.Tags.Insert(0, new Tag { Name = "first" });
  d.Tags.Clear();
}
```

To ask **where** something sits rather than change anything, `.IndexOf(item)`, `.LastIndexOf(item)` and
[`.FindIndex(x => …)`](https://osysharp.com/reference/query/in-memory-linq/) answer its position, or `-1` when it is not there. `IndexOf` finds the
first occurrence and `LastIndexOf` the last, which is the only thing they disagree about. Over a list of
entity rows all three compare by [row identity](https://osysharp.com/reference/entity/equality/), so `.IndexOf(row)` finds the row without your
comparing `.Id`.

Because it changes things, a change belongs in an `action` (or an `on-change` body), never in a `render` slot — a
render expression is re-evaluated whenever anything it reads changes, in an order nobody controls, so the compiler
refuses `.Add` / `.Remove` / `.RemoveAll` there and names where they go instead. The reads (`.Count`, `.Contains`,
`.Keys`, `.Values`, indexing, every LINQ verb) are render-slot material and always were.

### A collection inside a collection   {#nesting}
The grouping shape — a map of lists — is an ordinary field:

```osy title="a map of lists" test app=class-collections-nested
class Index {
  public Dictionary<string, List<int>> ByTag;
  public List<List<int>> Rows;
}

int TotalTagged(Index ix) {
  var n = 0;
  foreach (var kv in ix.ByTag) { n = n + kv.Value.Count; }
  return n;
}

// The inner list is a REAL list — reached through the pair and appended to in place.
void Tag(Index ix, string tag, int id) {
  if (!ix.ByTag.ContainsKey(tag)) { ix.ByTag[tag] = new List<int>(); }
  ix.ByTag[tag].Add(id);
}
```

### An enum element       {#enums}
An element may be an **enum you declared**, in any of the four spellings — including both slots of a map, which is how
a per-status tally is written:

```osy title="collections of an enum" test app=class-collections-enum
enum Status { Open, Closed }

class Board {
  public List<Status> Lanes;
  public Status[] Order;
  public Dictionary<Status, int> Tally;      // an enum KEY
  public Dictionary<string, Status> ByName;  // an enum VALUE
}

int OpenCount(Board b) {
  var n = 0;
  foreach (var s in b.Lanes) { if (s == Status.Open) { n = n + 1; } }
  return n;
}

int OpenTally(Board b) {
  return b.Tally.Where(kv => kv.Key == Status.Open).Sum(kv => kv.Value);
}
```

### Array or list?     {#array-or-list}
Use a **`List<T>`** when the contents change — it is what you will want most of the time. Use a **`T[]`** when the set
of values is settled once and then only read.

The difference is C#'s and the compiler holds you to it: an array's length is part of the type, so it has no `.Add`:

```osy syntax
d.Keywords.Add("x");   // ✗ `Add` needs a growable collection, and `string[]` is fixed-size
d.Tags.Add(tag);       // ✓ a List grows
```

Nothing else differs. Both are indexable, both count, both `foreach`, both answer every LINQ verb — so choosing an
array never costs you a way to read it.

### Can an entity hold a list?   {#entities}
This is a `class` surface, and deliberately. On an [entity](https://osysharp.com/reference/entity/declaration/) a collection member means something
else entirely: `OrderLine[] Lines` is a **relation** — a set of child ROWS, stored in their own table and reached
through a foreign key. A column cannot hold a list, so `string[] Tags;` on an entity is refused, and it names the two
real answers:

- **Model each value as a row.** A `DocumentTag` entity holding one `string`, reached as
  `[ForeignKey(Document)] DocumentTag[] Tags;` — which is queryable, indexable, and the answer whenever you will ever
  want to search or count by it.
- **Or keep the list on a `class`**, when it is a value the row simply carries and nobody queries across.

See [relations](https://osysharp.com/reference/entity/relations/) for the relation form.

## See also       {#see-also}
- [LINQ over a local list](https://osysharp.com/reference/query/in-memory-linq/) — the verbs that read these fields, and the same verbs over a local sequence
- [class properties](https://osysharp.com/reference/class/properties/) — a member that runs a body on access
- [relations](https://osysharp.com/reference/entity/relations/) — what a collection member means on an entity, and why it is a different thing


---

<!-- https://osysharp.com/reference/class/type-tests/ -->

# Testing which class a value is

> `s is Circle` asks which type a value actually is at run time, answering by the value's own type rather than the type it is declared as. A derived value satisfies its base, `null` is of no type, and `is not` negates the test. `OfType<T>()` asks the same question of a whole set, keeping the elements that are a `T` and re-typing them. `(T)value` and `value as T` convert to the narrower type — failing loudly, or answering null.

<!-- id: class-type-tests · area: class · stability: stable · html: https://osysharp.com/reference/class/type-tests/ -->

## Summary        {#summary}
`is` asks what a value **actually** is, which is not always what it is declared as:

```osy title="asking which shape it is" test app=class-type-test
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

string WhatIsIt() {
  Shape s = new Circle { Name = "c", Radius = 2m };
  if (s is Circle) { return "circle"; }
  return "shape";
}
```

The slot says `Shape`; the value is a `Circle`, and `is` answers by the value.

## Signature      {#signature}
```osy syntax
value is Class          // true when the value's runtime type is Class, or derives from it
value is not Class      // the negation
```

## Description    {#description}

### It answers by the RUNTIME type   {#runtime-type}
That is the whole point — a declared type is what the compiler knows, and `is` is for what the compiler cannot know.
A value that really is a plain `Shape` answers false:

```osy title="the test discriminates" test app=class-type-test-negative
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

string WhatIsIt() {
  Shape s = new Shape { Name = "plain" };
  if (s is Circle) { return "circle"; }
  return "shape";
}
```

### A derived value satisfies its base   {#derived}
`is Shape` accepts the whole subtree beneath `Shape`, not only an exact `Shape`. This is what makes it a *type test*
rather than a comparison of labels:

```osy title="a Circle IS a Shape" test app=class-type-test-derived
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

string Check() {
  Shape s = new Circle { Name = "c", Radius = 2m };
  if (s is Shape) { return "yes"; }
  return "no";
}
```

### `null` is of no type   {#null}
As in C#, `null` is not an instance of anything — so `null is Circle` is false, and `null is not Circle` is true:

```osy title="null satisfies no type test" test app=class-type-test-null
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

string Check() {
  Shape? s = null;
  if (s is not Circle) { return "not-a-circle"; }
  return "circle";
}
```

### Narrowing a whole collection — `OfType<T>()`   {#oftype}
`OfType<T>()` keeps the elements that are a `T` and gives you them **as** a `T`:

```osy title="a mixed list narrowed to one type" test app=class-type-test-oftype
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

decimal TotalRadius() {
  List<Shape> shapes = new List<Shape>();
  shapes.Add(new Circle { Name = "a", Radius = 1m });
  shapes.Add(new Shape { Name = "b" });
  shapes.Add(new Circle { Name = "c", Radius = 3m });

  decimal total = 0m;
  foreach (var c in shapes.OfType<Circle>()) { total = total + c.Radius; }
  return total;                                   // 4 — the plain Shape is not in the set
}
```

It does two things at once, and the second is why you would reach for it over `Where`: it **filters** to the
elements that really are a `Circle`, and it **re-types** them, so `c.Radius` reads. `shapes.Where(s => s is Circle)`
filters identically but its result is still a `List<Shape>` statically, so a derived field is out of reach.

It only ever narrows. Asking for the element's own base, or for a type outside its hierarchy, is refused rather than
quietly widening the read or returning nothing.

### Converting to the narrower type — cast or `as`?   {#cast-and-as}
A type test answers *whether*; a **conversion** hands you the value at the narrower type. Two spellings, differing
only in what happens when the value is not that type:

```osy title="the cast and its try-conversion" test app=class-type-test-cast
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

decimal RadiusOrZero(Shape s) {
  Circle? maybe = s as Circle;              // null when it is not a Circle
  if (maybe == null) { return 0m; }
  return maybe.Radius;
}

decimal RadiusOf(Shape s) {
  Circle c = (Circle)s;                     // FAILS when it is not a Circle
  return c.Radius;
}
```

| | when it IS the type | when it is NOT |
|---|---|---|
| `(Circle)s` | the value, typed `Circle` | **fails**, naming both types |
| `s as Circle` | the value, typed `Circle?` | `null` |

Use the cast when being wrong is a bug you want to hear about, and `as` when "not a Circle" is an ordinary case you
are about to handle. That is the same advice C# gives, for the same reason.

**`null` converts to `null` under both, and neither fails.** A cast of null is not a failed cast — there is nothing
there to be of the wrong type.

Widening needs no conversion at all: a `Circle` already goes wherever a `Shape` is expected
([[class-inheritance#upcast]]).

### Dispatching on the type — `switch`   {#switch}
A `switch` arm can be a **type pattern** — `Circle c =>` — binding the value at that arm's own type:

```osy title="one arm per type" test app=class-type-test-switch
class Shape { public string Name; }
class Circle : Shape { public decimal Radius; }

string Describe() {
  Shape s = new Circle { Name = "c", Radius = 2m };
  return s switch {
    Circle c => "circle:" + c.Radius.ToString(),   // `c` is a Circle here
    Shape p  => "shape:" + p.Name,
  };
}
```

The binding is **required** — write `Circle c =>`, not `Circle =>`. A bare name in pattern position is already an
enum member, and the binding is what tells the two apart.

#### Every type must be handled   {#exhaustive}
A `switch` with no `_` arm must have an arm for **every** type in the hierarchy. Leave one out and the app does not
compile, and the message names what is missing:

```osy syntax
return s switch {
  Circle c => "circle",          // ERROR: does not handle every 'Shape' — 'Shape' has no arm
};
```

This is stricter than C#, which only warns — and deliberately so. C#'s compiler cannot see hierarchies that other
assemblies might extend, so it cannot know the set is complete. An Osy# app compiles as **one unit**, so the set of
types *is* known. What that buys you is the useful half: **adding a type breaks every place that has to decide about
it**, instead of those places silently taking a default that was never considered.

Add `_ => …` when the rest really are the same — that says so explicitly, and it is not second-best.

### It works the same in the browser   {#both-sides}
A type test is decided identically wherever the code runs — in a function on the server, in a component in the
browser, and across a suspension that starts on one side and resumes on the other. There is no rule to learn about
where you may write it.

### Entities test their type too   {#entities}
`is` works on an [entity hierarchy](https://osysharp.com/reference/entity/inheritance/) as well, and reads the same. The two are decided by
different means — an entity is a row and carries its type in a column, which is what lets an entity's test run inside
a database query — but nothing about writing one differs.

## Errors         {#errors}
| you wrote | what you get |
|---|---|
| `x is Unrelated`, where the two share no hierarchy | refused — no value can be both, so the answer would be a constant you did not write |
| `x is Solo`, where `Solo` has no base and no subtypes | refused — a type test is only meaningful inside a hierarchy |
| `(Circle)s` where `s` is not a `Circle` | fails at run time, naming both types and pointing at `is` / `as` |
| `(Circle)s` where the two share no hierarchy | refused at compile time — no value can be both |
| `(Contract)order` between two ENTITY types | refused — a row already carries its type; narrow the READ with `OfType` |
| `xs.OfType<Circle>()` where `Circle` does not derive from the element type | refused — no element of the set could be one |
| `xs.OfType<Shape>()` where `Shape` is the element's BASE | refused — that would WIDEN the read, not narrow it |

## See also       {#see-also}
- [Class inheritance](https://osysharp.com/reference/class/inheritance/) — declaring the hierarchy a type test asks about
- [entity Sub : Base](https://osysharp.com/reference/entity/inheritance/) — the same question asked of rows
- [Classes](https://osysharp.com/reference/class/index/) — what a class is


---

<!-- https://osysharp.com/reference/class/params/ -->

# `params` parameters

> `params` lets a method be called with any number of trailing arguments — `Total(1m, 2m, 3m)` — which arrive as one array. It must be the last parameter and its type must be an array. Calling with no trailing arguments passes an empty array, so a `params` parameter is optional without a default; passing an actual array of the right type still binds directly, without being wrapped again.

<!-- id: class-params · area: class · stability: stable · html: https://osysharp.com/reference/class/params/ -->

## Summary        {#summary}
`params` collects the trailing arguments into an array:

```osy title="any number of arguments" run app=class-params
class Calc {
  public decimal Total(params decimal[] xs) {
    decimal t = 0m;
    foreach (var x in xs) { t = t + x; }
    return t;
  }
}

[Test]
void Params_Collects_The_Trailing_Arguments() {
  var c = new Calc();
  Assert.Equal(6m, c.Total(1m, 2m, 3m));
  Assert.Equal(0m, c.Total());              // no trailing arguments — an EMPTY array, not null
}
```

## Signature      {#signature}
```osy syntax
<Return> <Name>(params <T>[] <rest>) { … }
<Return> <Name>(<T> <first>, params <T>[] <rest>) { … }
```

## Description    {#description}

### Where can `params` go, and what type must it be?   {#rules}
Two rules, both C#'s, and both following from what `params` does — it collects *the rest*:

```osy title="✗ the three ways params is written wrong" syntax
decimal Sum(params decimal[] xs, string tail) { … }   // ✗ it must be the LAST parameter
decimal Sum(params decimal x) { … }                   // ✗ its type must be an ARRAY
decimal Sum(params decimal[] xs = null) { … }         // ✗ it is already optional; no default
```

A parameter before it is ordinary and keeps its own argument:

```osy title="a fixed parameter, then the rest" run app=class-params
class Report {
  public decimal Offset(decimal start, params decimal[] xs) {
    decimal t = start;
    foreach (var x in xs) { t = t + x; }
    return t;
  }
}

[Test]
void The_Leading_Parameter_Keeps_Its_Own_Argument() {
  var r = new Report();
  Assert.Equal(103m, r.Offset(100m, 1m, 2m));
}
```

### Passing an array directly still works   {#an-actual-array}
If you already have the array, pass it — it binds to the parameter as-is rather than being wrapped in another array:

```osy title="an array binds directly" run app=class-params
[Test]
void An_Actual_Array_Is_Not_Wrapped_Again() {
  var c = new Calc();
  decimal[] values = [4m, 5m];
  Assert.Equal(9m, c.Total(values));       // one array of two — not one array containing one array
}
```

Which of the two readings applies is decided by the argument's **type**, not by how many arguments there are — both
spellings pass exactly one. A `decimal[]` is the array; a `decimal` is one element of it.

### `params` and overloads   {#overloads}
A `params` method takes part in [overload resolution](https://osysharp.com/reference/class/overloads/) like any other, with one rule: the ordinary
reading is tried first for every candidate, and the collecting form is considered only if nothing matched ordinarily.

That has a consequence worth relying on: **adding `params` to an existing method cannot change which overload an
existing call already picks.** It can only make a call compile that did not before.

```osy title="an exact match wins over collecting" run app=class-params
class Fmt {
  public string Of(decimal d) { return "one"; }
  public string Of(params decimal[] ds) { return "many"; }
}

[Test]
void The_Ordinary_Reading_Is_Tried_First() {
  var f = new Fmt();
  Assert.Equal("one", f.Of(1m));            // the exact single-argument method
  Assert.Equal("many", f.Of(1m, 2m));       // only the collecting one can take two
}
```

`params` is **not** part of a method's identity, so `Sum(params decimal[])` and `Sum(decimal[])` are the same method
and cannot both be declared — a call site could not tell them apart.

### What is the collected array, inside the body?   {#no-new-shape}
Inside the body, `rest` is an ordinary array: `foreach` over it, ask for its `Count()`, index it, pass it on. There is
nothing to learn beyond the call form.

## See also       {#see-also}
- [class methods](https://osysharp.com/reference/class/methods/) — methods, and the member-body surface
- [Method overloads](https://osysharp.com/reference/class/overloads/) — how a call site picks from a set of same-named methods
- [`static` methods](https://osysharp.com/reference/class/static/) — `static`, which combines with `params` freely
- [Classes](https://osysharp.com/reference/class/index/) — fields, `const`, and what a class is


---

<!-- https://osysharp.com/reference/class/readonly/ -->

# `readonly` fields

> A `readonly` field can be assigned only where it is declared or in a constructor of the class that declares it. Everywhere else — a method of the same class, a subclass constructor, an object initializer, any code holding the value — a write is a compile error. The field itself is ordinary: it holds a per-instance value chosen at construction, unlike a `const`, whose value is fixed when the code is compiled.

<!-- id: class-readonly · area: class · stability: stable · html: https://osysharp.com/reference/class/readonly/ -->

## Summary        {#summary}
`readonly` marks a field that only the constructor may set:

```osy title="a field the constructor fixes" test app=class-readonly
class Booking {
  public readonly string Reference;
  public readonly int Seats;

  public Booking(string reference, int seats) {
    Reference = reference;
    Seats = seats;
  }
}
```

Once the object exists, the field is settled — every other write is refused at compile time.

## Signature      {#signature}
```osy syntax
readonly T Name;                // set by a constructor
readonly T Name = value;        // set at the declaration
public readonly T Name;         // combines with any visibility
```

## Description    {#description}

### The two places a `readonly` field may be assigned   {#where}
There are exactly two, and they are the same two as in C#:

1. **its own declaration** — `public readonly decimal Rate = 0.25m;`
2. **a constructor of the class that declares it**

```osy title="both legal writes" test app=class-readonly
class Invoice {
  public readonly decimal Rate = 0.25m;      // 1. at the declaration
  public readonly string Number;

  public Invoice(string number) {
    Number = number;                          // 2. in the constructor
  }
}
```

Both values are ordinary per-instance data — read them like any other field:

```osy title="a readonly field holds a normal runtime value" run app=class-readonly
[Test]
void ReadOnly_Fields_Hold_Their_Values() {
  var i = new Invoice("INV-1");
  Assert.Equal("INV-1", i.Number);
  Assert.Equal(0.25m, i.Rate);
}
```

### Assigning it anywhere else is a compile error   {#refused}
The refusals are the feature. None of these compile:

```osy syntax
var i = new Invoice("INV-1");
i.Number = "INV-2";                  // ✗ a write from outside
i.Rate += 0.1m;                      // ✗ `+=` is a write too

var j = new Invoice { Number = "x" };   // ✗ an object initializer runs after the constructor

class Invoice {
  public void Renumber(string n) {
    Number = n;                      // ✗ a METHOD of the same class — only a constructor may write
  }
}
```

The last one is worth pausing on: `readonly` narrows the write to **constructors**, not to the class. A method of the
declaring class is refused exactly like outside code.

### A subclass constructor cannot write the base's field   {#inheritance}
A field belongs to the class that declares it. By the time a derived constructor's body runs, the base has already
been constructed and its `readonly` fields are settled — so a subclass may write its **own** readonly fields and not
its base's:

```osy title="each class writes the fields it declares" test app=class-readonly
class Badge {
  public readonly string Tag;
  public Badge(string tag) { Tag = tag; }
}

class Ranked : Badge {
  public readonly decimal Rank;

  public Ranked(string tag, decimal rank) : base(tag) {
    Rank = rank;                     // its own — fine
  }
}
```

Writing `Tag = "x"` inside `Ranked`'s constructor is a compile error: pass the value to `base(…)` instead, which is
what the example does.

### `readonly` is not `const`   {#versus-const}
They read similarly and mean different things:

| | `const` | `readonly` |
|---|---|---|
| when the value is chosen | when the code is compiled | when the object is constructed |
| can it differ per instance | no — there is one value | yes — each `new` may pass a different one |
| what it may be initialized with | a compile-time constant | any expression the constructor can evaluate |
| is there a per-instance slot | no, uses are replaced by the value | yes, it is a real field |

Reach for `const` for a fixed number or name the whole program shares, and `readonly` for a value each instance is
given once and then keeps. Writing both on one field is a compile error — a `const` has no instance slot to protect.

### `readonly` applies to a field, not a property   {#not-a-property}
A property has no storage of its own, so there is nothing for `readonly` to narrow. The property spellings that mean
the same things are:

- `{ get; }` — an [auto-property](https://osysharp.com/reference/class/properties/) only a constructor may set
- `{ get; init; }` — settable during construction, including from an object initializer

```osy syntax
public readonly decimal Area { get; set; }   // ✗ readonly applies to a FIELD
public decimal Area { get; }                 // ✓ only the constructor sets it
public decimal Area { get; init; }           // ✓ an object initializer may too
```

### Can an entity field be `readonly`?   {#entities}
`readonly` is a `class` modifier. An entity's fields are stored rows written by data operations, and what may write
them is declared in its `security { }` block rather than by a member modifier.

## See also       {#see-also}
- [class properties](https://osysharp.com/reference/class/properties/) — `init`, `required`, and the property forms that express single-assignment
- [constructor](https://osysharp.com/reference/class/constructors/) — the constructor, which is where a `readonly` field gets its value
- [Classes](https://osysharp.com/reference/class/index/) — fields, defaults, and what a class is
- [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/) — `public`, `private` and `protected`, which combine with `readonly`


---

<!-- https://osysharp.com/reference/class/static/ -->

# `static` methods

> A `static` method belongs to the type rather than to any instance, and is called on the type name — `Money.Round(x)`. It has no `this`, so it cannot read the class's instance fields; it can call the class's other static methods and read its `const` values by bare name. Fields cannot be static: a `const` covers the fixed values, and anything that would need to change belongs to an instance or to an entity.

<!-- id: class-static · area: class · stability: stable · html: https://osysharp.com/reference/class/static/ -->

## Summary        {#summary}
A `static` method is called on the type, not on a value:

```osy title="a static method, called on the type name" test app=class-static
class Money {
  public decimal Amount;

  public static decimal Round(decimal value) {
    return Math.Round(value, 2);
  }
}

decimal RoundAPrice() {
  return Money.Round(19.999m);       // on the TYPE — there is no Money instance here
}
```

Reach for it when the operation is about the type but not about any particular value of it — a conversion, a
validation, a calculation over its arguments.

## Signature      {#signature}
```osy syntax
static <Return> <Name>(<params>) { … }
public static <Return> <Name>(<params>) { … }
```

`static` goes after the visibility word, as in C#. It applies to methods only.

## Description    {#description}

### A static method has no `this`   {#no-this}
That is the whole of the rule, and everything else follows from it. There is no instance, so there is nothing for an
instance field to be read from:

```osy title="✗ a static method reaching for instance state" syntax
class Money {
  public decimal Amount;

  public static decimal Doubled() {
    return Amount * 2m;              // ✗ 'Amount' belongs to an INSTANCE
  }

  public static decimal Half() {
    return this.Amount / 2m;         // ✗ `this` has no meaning in a static method
  }
}
```

Take the value as a parameter instead — which is usually what the method wanted anyway:

```osy title="pass in what the method needs" test app=class-static
class Tax {
  public static decimal Net(decimal gross, decimal rate) {
    return gross / (1m + rate);
  }
}
```

### What a static method CAN see of its own class   {#what-it-sees}
Its other static methods and its `const` values, both by bare name — the same rule as C#:

```osy title="statics reach statics, and consts, unqualified" run app=class-static
class Rate {
  public const decimal Standard = 0.25m;

  public static decimal Apply(decimal amount) {
    return amount * (1m + Standard);      // a const, by bare name
  }

  public static decimal ApplyTwice(decimal amount) {
    return Apply(Apply(amount));          // another static, by bare name
  }
}

[Test]
void Statics_Reach_Statics_And_Consts() {
  Assert.Equal(125m, Rate.Apply(100m));
  Assert.Equal(156.25m, Rate.ApplyTwice(100m));
}
```

An **instance** method may call its class's static methods the same way — it simply does not pass its `this` along:

```osy title="an instance method calling its type's static" test app=class-static
class Line {
  public decimal Gross;

  public decimal Net() {
    return Tax.Net(Gross, 0.25m);
  }
}
```

### Call it on the type, never through a value   {#call-form}
The two directions are both compile errors, and each says which spelling to use:

```osy syntax
var m = new Money { Amount = 1m };

m.Round(2.5m);          // ✗ 'Money.Round' is static — call it as `Money.Round(…)`
Money.Amount;           // ✗ 'Money.Amount' is an instance member — access it through an instance
```

Refusing the first is C#'s rule too, and it is worth the strictness: `m.Round(…)` reads as though the method can see
`m`, and it cannot — the receiver would be evaluated and discarded.

### Statics inherit   {#inheritance}
A static method declared on a base class is callable on a derived one, like any other inherited member:

```osy title="a subclass inherits its base's statics" test app=class-static
class Shape {
  public string Name;
  public static decimal Zero() { return 0m; }
}

class Circle : Shape {
  public decimal Radius;
}

decimal ZeroThroughTheSubclass() {
  return Circle.Zero();          // Shape declares it; Circle inherits it
}
```

It cannot be `virtual`, `override` or `abstract`, and combining them is a compile error. Those words choose a body
from the *receiver's* runtime type, and a static call has no receiver to choose by.

### Static methods overload   {#overloads}
Exactly like instance methods — a name maps to a set, and the call site picks by the arguments. See
[Method overloads](https://osysharp.com/reference/class/overloads/) for the rule.

```osy title="two statics sharing a name" test app=class-static
class Fmt {
  public static string Of(decimal d) { return d.ToString(); }
  public static string Of(decimal d, string unit) { return d.ToString() + " " + unit; }
}
```

### Fields cannot be static   {#no-static-fields}
`static` applies to methods only. A static **field** would be mutable state shared by every application running in
the host process, with no per-app copy to reset — so it is refused, in as many words:

```osy syntax
class Counter {
  public static decimal Total;         // ✗ cannot be `static`
}
```

There are three things people reach for it for, and each has its own answer:

| what you want | reach for |
|---|---|
| a fixed value the whole program shares | `const` — already static, and needs no modifier |
| a value that belongs to one object | an ordinary field |
| state that outlives a request | an `entity` — stored, per-app, and secured |

For the same reason there is no static **constructor**: it would exist to initialize static state, and there is none.

## See also       {#see-also}
- [class methods](https://osysharp.com/reference/class/methods/) — instance methods, and the member-body surface a static method shares
- [Method overloads](https://osysharp.com/reference/class/overloads/) — how a call site picks from a set of same-named methods
- [Classes](https://osysharp.com/reference/class/index/) — fields, `const`, and what a class is
- [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/) — `public`, `private` and `protected`, which combine with `static`


---

<!-- https://osysharp.com/reference/class/methods/ -->

# class methods

> Behaviour attached to a class — a method with a receiver, called as value.Method(). Classes are in-memory values, so a method is the natural place for logic that belongs to a shape rather than to the database.

<!-- id: class-methods · area: class · stability: stable · html: https://osysharp.com/reference/class/methods/ -->

## Summary        {#summary}
A `class` may declare **methods**, called on a value: `cart.Total()`. A method has a receiver — the instance it was
called on — and can read and write that instance's fields.

Use a method when the logic belongs to the *shape*. Use a [function](https://osysharp.com/reference/function/declaration/) when it belongs to the
application.

## Signature      {#signature}
```osy syntax
class <Name> {
  public <Type> <Field>;
  public <Return> <Method>(<params>) { … }   // reads/writes this instance's fields
}
```

## Description    {#description}

### A method reads its own fields   {#fields}
Inside a method the class's fields are in scope by name — there is no ceremony:

```osy title="a class that can total itself" test app=class-methods
class Line {
  public string Sku;
  public int Qty;
  public decimal UnitPrice;

  public decimal Total() {
    return Qty * UnitPrice;
  }
}

decimal LineTotal(string sku, int qty, decimal price) {
  var line = new Line { Sku = sku, Qty = qty, UnitPrice = price };
  return line.Total();
}
```

### A method can mutate the instance   {#mutation}
A class is an in-memory value, so a method may change it. Nothing is persisted — there is no row behind it:

```osy title="a method that changes the value" test app=class-methods
class Basket {
  public decimal Total;
  public int Count;

  public void Add(decimal amount) {
    Total = Total + amount;
    Count = Count + 1;
  }

  public decimal Average() {
    return Count == 0 ? 0m : Math.Round(Total / Count, 2);
  }
}

decimal AverageOfThree(decimal a, decimal b, decimal c) {
  var basket = new Basket { };
  basket.Add(a);
  basket.Add(b);
  basket.Add(c);
  return basket.Average();
}
```

### Can a component hold one as state?   {#component-state}
A component field can hold a class instance, and its methods are callable from the component's actions and lifecycle
hooks like any other value. This is how a small piece of behaviour — a gate, a counter, a tiny state machine — lives
beside the page that uses it rather than being spread across loose fields.

The worked example is a **cooldown**: "don't do this again until N has passed", which has no cadence of its own and so
is not what [on every](https://osysharp.com/reference/ui/cadence/) is for.

```osy title="a cooldown gate, held as component state" test app=class-methods-component
class Cooldown {
  public DateTime ReadyAt;

  /// True once the wait has passed — then `Arm` starts the next one.
  public bool Ready() { return DateTime.UtcNow >= ReadyAt; }
  public void Arm(TimeSpan wait) { ReadyAt = DateTime.UtcNow + wait; }
}

[Page("/cooldown")]
[AllowAnonymous]
component Repeater() {
  // Ready immediately — a field is required by default, so it is given a value at the create site.
  Cooldown gate = new Cooldown { ReadyAt = DateTime.UtcNow };
  int fired = 0;

  action Nudge() {
    // Held down, this fires at most once every 90ms rather than once per event.
    if (gate.Ready()) {
      fired = fired + 1;
      gate.Arm(TimeSpan.FromMilliseconds(90));
    }
  }

  render {
    Stack(gap: 2) {
      Text($"fired {fired}");
      Pressable("nudge", onClick: Nudge);
    }
  }
}
```

Reading a field (`gate.ReadyAt`), assigning one (`gate.ReadyAt = …`) and calling a method (`gate.Ready()`) all work on
such a field. The instance is ordinary component state: it lives as long as the component does, and it is not
persisted.

### Method or function?   {#method-or-function}
| | class method | top-level function |
|---|---|---|
| Has a receiver | **yes** — the instance it is called on | no |
| Called as | `value.Method()` | `Method(value)` |
| Can write rows | no — a class has no table | **yes** |
| Good for | logic that belongs to a shape | logic that belongs to the app |

An [entity](https://osysharp.com/reference/entity/declaration/) cannot have methods: an entity body holds data, and the behaviour that acts on it is
a top-level function. That split is deliberate — it keeps the thing that is persisted separate from the thing that is
merely computed.

## See also       {#see-also}
- [Method overloads](https://osysharp.com/reference/class/overloads/) — declaring several methods with one name, and how a call picks between them
- [on every](https://osysharp.com/reference/ui/cadence/) — `on every`, for behaviour that repeats on a clock rather than waiting to be asked
- [constructor](https://osysharp.com/reference/class/constructors/) — building an instance with arguments
- [function](https://osysharp.com/reference/function/declaration/) — behaviour that belongs to the application, not to a shape
- [entity](https://osysharp.com/reference/entity/declaration/) — why an entity has no methods


---

<!-- https://osysharp.com/reference/class/properties/ -->

# class properties

> A class member that reads and writes like a field but runs a body on access — a computed value, a validating setter, or an auto-property whose storage the platform synthesizes. It is a method dressed as a field access, so it works everywhere a class does: server, client, and across a suspension.

<!-- id: class-properties · area: class · stability: stable · html: https://osysharp.com/reference/class/properties/ -->

## Summary        {#summary}
A `class` may declare **properties** — members that read and write like a field but run a body on access. A property
is the natural home for a value *derived* from other fields (`Label`), for a write that must be *validated* or
*transformed*, and for a field you want to expose with asymmetric access. It is, exactly as in C#, a pair of methods
dressed as a field: a read `obj.Prop` runs the getter, a write `obj.Prop = v` runs the setter.

Use a plain [field](https://osysharp.com/reference/class/methods/) when a value is just stored; reach for a property when *access* itself is behaviour.

## Signature      {#signature}
```osy syntax
class <Name> {
  public <Type> <Prop> => <expr>;                 // computed — read-only, no storage
  public <Type> <Prop> { get => <expr>; }         // getter-only, explicit
  public <Type> <Prop> { get => …; set => …; }    // full property over a backing field (`value` is the input)
  public <Type> <Prop> { get; set; }              // auto-property — the platform synthesizes the backing field
  public <Type> <Prop> { get; private set; }      // asymmetric — read anywhere, write only inside the class
  public <Type> <Prop> { get; init; }             // init-only — settable during construction, then frozen
  public required <Type> <Prop> { get; set; }     // required — must be supplied in every `new T { … }`
}
```

## Description    {#description}

### A computed property — a value derived from other fields   {#computed}
The smallest property is **getter-only**: an expression over the instance's other fields, with no storage of its own.
Written `=> expr`, it recomputes on every read.

```osy title="a value derived from other fields" test app=class-properties
class Money {
  public decimal Amount;
  public string Currency;

  // computed on read — no backing storage
  public string Label => Amount.ToString("F2") + " " + Currency;
}
```

Reading it runs the getter over the current field values:

```osy title="the getter runs on read" run app=class-properties
[Test]
void Label_Runs_The_Getter() {
  var m = new Money { Amount = 9.5m, Currency = "USD" };
  Assert.Equal("9.50 USD", m.Label);
}
```

A computed property is read-only — it has no setter, so a write to it is a compile error. That is the point: it is a
view of other state, not a slot you can assign.

### A full property — a getter and a setter over a backing field   {#get-set}
When a write needs to be *validated* or *transformed*, give the property both accessors over an explicit private
field. Inside the setter, the incoming value is the implicit parameter `value`:

```osy title="a validating setter over a backing field" test app=class-properties
class Account {
  private decimal _rate;

  public decimal Rate {
    get => _rate;
    set {
      if (value < 0m) { throw "rate cannot be negative"; }
      _rate = value;
    }
  }
}
```

A write runs the setter; a read runs the getter:

```osy title="the setter runs on write, the getter on read" run app=class-properties
[Test]
void Rate_RoundTrips_Through_The_Accessors() {
  var a = new Account();
  a.Rate = 0.2m;
  Assert.Equal(0.2m, a.Rate);
}
```

A bad write — `a.Rate = -1m` — runs the setter body and throws, exactly as the setter says. The setter is the one
place the rule lives, so no caller can slip an invalid value past it.

### An auto-property — the platform synthesizes the storage   {#auto}
When the accessors would be trivial — read the field, write the field — write `{ get; set; }` and the platform
synthesizes the hidden backing field for you:

```osy title="an auto-property" test app=class-properties
class Contact {
  public string Name { get; set; }
  public string Email { get; set; }
}
```

It stores and reads a value like a field — both through assignment and through an object initializer:

```osy title="an auto-property stores a value" run app=class-properties
[Test]
void Auto_Property_Stores_A_Value() {
  var c = new Contact { Name = "Ada", Email = "ada@example.com" };
  Assert.Equal("Ada", c.Name);

  c.Name = "Grace";
  Assert.Equal("Grace", c.Name);
}
```

### Asymmetric visibility — read anywhere, write only inside   {#asymmetric}
A `private set` narrows the *write* path without touching the read path: anyone can read the property, but only the
class's own code can set it. It reuses the same private-member rule as a private method.

```osy title="read anywhere, write only inside the class" test app=class-properties
class Ledger {
  public decimal Balance { get; private set; }

  // in-class code sets it through the private setter
  public void Credit(decimal amount) {
    Balance = Balance + amount;
  }
}
```

From outside `Ledger`, `ledger.Balance = 100m` is a compile error — the setter is private — while `ledger.Balance`
reads freely. A bare `{ get; }` behaves the same way: settable inside the class, read-only to the outside.

### `init` — settable only during construction   {#init}
An `init` accessor is a setter you may run **only while the object is being built** — in an object initializer or the
constructor — and never after. It is how you make a value that is fixed once constructed:

```osy title="a value fixed at construction" test app=class-properties
class Booking {
  public string Reference { get; init; }
  public int Seats { get; init; }
}
```

Set it in the object initializer; a write afterwards is a compile error:

```osy title="init is set at construction, then frozen" run app=class-properties
[Test]
void Init_Is_Set_At_Construction() {
  var b = new Booking { Reference = "BK-1", Seats = 2 };
  Assert.Equal("BK-1", b.Reference);
  Assert.Equal(2, b.Seats);
}
```

Writing `b.Reference = "BK-2"` **after** construction does not compile — the accessor is init-only. Reach for `init`
when a field must be supplied when the object is made but must not change once it exists. A bodied `init { … }` runs
its body during construction, so it can validate or transform the incoming value exactly like a `set`.

### `required` — must be supplied at every `new`   {#required}
Marking a member `required` makes the compiler insist it appears in **every** object initializer — a missing one is a
compile error, not a value silently left null:

```osy title="a member the caller must supply" test app=class-properties
class Registration {
  public required string Email { get; set; }
  public string? Name { get; set; }   // optional (reads back null when unset) — a bare `string Name` would itself be required
}
```

```osy title="required is enforced at the call site" run app=class-properties
[Test]
void Required_Is_Supplied() {
  var r = new Registration { Email = "ada@example.com" };   // Name is optional; Email is required
  Assert.Equal("ada@example.com", r.Email);
}
```

Omitting `Email` — `new Registration { }` — is a compile error. `required` pairs naturally with `init` for a value
that must be given once and then frozen: `public required string Email { get; init; }`.

A constructor that always sets a required member takes over that obligation **automatically** — the compiler sees the
constructor sets it, so `new T(args)` needs no initializer for it and no annotation:

```osy title="a constructor that satisfies required" test app=class-properties
class Membership {
  public required string Owner { get; set; }
  public Membership(string owner) { Owner = owner; }   // sets Owner on every path — the compiler infers it
}
```

```osy title="the constructor satisfies the requirement" run app=class-properties
[Test]
void Ctor_Satisfies_Required() {
  var m = new Membership("Ada");   // no `{ Owner = … }` needed — the ctor sets it
  Assert.Equal("Ada", m.Owner);
}
```

The inference is sound and conservative: the constructor must set the member **unconditionally** — directly, or through
a method it calls. If it only sets the member inside an `if`/loop, that isn't provable, so the initializer is still
required. A constructor that sets *some* required members can leave the rest to the caller's initializer
(`new Membership(owner) { OtherRequired = … }`). You may still write `[SetsRequiredMembers]` explicitly — it is accepted
and means exactly this.

### A member that lives in the browser   {#client-handles}
A class may hold the values that only exist on the client — the ones a drawing app builds and keeps:

```osy syntax
class Valley {
  Mesh ground;          // geometry, on the GPU
  Mesh hill;
  Gradient sky;         // a fill the 2D context owns
  Surface stars;        // an offscreen canvas
  Random rng;           // a random stream

  public void Build() { ground = Mesh.Plane(160, 40); }
  public void Paint() { Draw.Mesh(ground, 0, 0, 0, "#79b85c"); }
}
```

⭐ **This is what lets a drawing app be organised at all.** A class method can run the `Draw.*` verbs, so each part
of a scene can own its own geometry and know how to paint itself, in its own file — and the page is left with what
only it knows: the physics, the input, the score.

⚠️ **On a `class`, not on an `entity`.** These values live in the browser, so there is nothing for a stored row to
hold; declaring one on a persisted `entity` is refused, and the message says so.

### It runs on the client too   {#client}
A property is a method call underneath, so it rides the same path a [method](https://osysharp.com/reference/class/methods/) does: a getter read or
setter write inside a `[Render(CSR)]` component action runs **in the browser**, with no server round trip, as long as
its body is client-runnable. Nothing extra is needed — the accessor ships with the component.

## See also       {#see-also}
- [`readonly` fields](https://osysharp.com/reference/class/readonly/) — `readonly` fields, the field-level counterpart to `{ get; }` and `init`
- [class methods](https://osysharp.com/reference/class/methods/) — behaviour with a receiver; a property is a field-shaped pair of these
- [constructor](https://osysharp.com/reference/class/constructors/) — set up an instance's fields (and back an auto-property) at construction
- [Classes](https://osysharp.com/reference/class/index/) — the whole `class` surface: fields, constructor, methods, properties
- [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/) — `public`/`private` on members, and why a class defaults to `internal`


---

<!-- https://osysharp.com/reference/class/constructors/ -->

# constructor

> A class declares one constructor — its name is the class name, it takes no return type, and it runs when you write new T(args). The constructor body runs first; object initializers { Member = value } apply after it; new T(args) evaluates to the constructed object.

<!-- id: class-constructors · area: class · stability: stable · html: https://osysharp.com/reference/class/constructors/ -->

## Summary        {#summary}
A **constructor** initializes a `class` instance. It is declared as a member whose name is the class name and
which has **no return type**; you invoke it with `new T(args)`. The constructor **body runs first**, then any
object initializers `{ Member = value }` are applied **on top** (so an initializer wins over a value the body
set). `new T(args)` evaluates to the constructed object.

## Signature      {#signature}
```osy syntax
class T {
  public T(<Type> p1, <Type> p2 = <default>) {   // no return type
    <statements>                                  // may read the params and assign this fields
  }

  public T() : this(<a>, <b>) { … }              // another one, chaining to the first
}

new T(a, b)                 // run the constructor these arguments select
new T(a, b) { Note = "x" }  // …then apply the initializer
```

## Description    {#description}
- **A class may declare several constructors**, told apart by their parameter TYPES exactly as
  [methods](https://osysharp.com/reference/class/overloads/) are — `new T(args)` picks the one its arguments fit. Two that differ only in
  parameter NAMES are the same constructor declared twice, and a compile error. The name is the class name and
  there is **no return type** (a `return;` with a value is a compile error).
- **`: this(…)` chains to another constructor of the same class**, which runs FIRST — so shared setup lives in one
  place. The chained-to constructor is the one that runs `: base(…)`; a chaining constructor does not also run the
  base's, so a base constructor runs exactly once. A chain that comes back round to where it started is a compile
  error rather than unbounded recursion at construction time.
- **Visibility** follows the class-member rule: `public` makes it callable from anywhere; a bare (unmarked)
  constructor is **private** — callable only from within its own class (the factory-method pattern).
- **`new T(args)`** binds the arguments to the constructor's parameters (positional or `name: value`, with C#
  default-parameter values for omitted optionals), runs the body with the fresh instance in scope, and
  produces that instance.
- **Initializers run after the body**: `new T(args) { Member = value }` runs the constructor first, then
  assigns each initializer — so an initializer overrides whatever the constructor set for that member (C#
  order). Initializer values cannot reference the object being constructed.
- **A class with no declared constructor** uses the implicit default: `new T()` and `new T { … }` build the
  instance with no user code. Passing arguments to such a class is a compile error.
- **A field initializer runs before every constructor body** (`decimal Rate = 0.25m;`), so the body reads the
  declared value and may overwrite it — C#'s order. Object initializers are the ones that run after.
- **A required parameter must be supplied**: if the constructor has a non-optional parameter, `new T { … }`
  (no arguments) is a compile error — pass the argument.
- Durable: a suspension **inside** a constructor body survives serialize/restore, and `new T(args)` still
  evaluates to the same constructed instance on resume.

## Examples       {#examples}
```osy title="a class with a constructor" test app=constructors
class Order {
  public int Qty;
  public decimal Total;
  public string Note;
  // The ctor assigns every required member, so `new Order(5, 2m)` compiles without also naming them in an
  // initializer — the compiler infers that the constructor satisfies them (no annotation needed).
  public Order(int qty, decimal price) {
    Qty = qty;
    Total = qty * price;
    Note = "new order";
  }
}

// The constructor sets the fields from its arguments.
decimal OrderTotal() {
  var o = new Order(5, 2m);
  return o.Total;                 // 10  (5 * 2, set in the body)
}

// An initializer is applied AFTER the body — so it wins.
string OrderNote() {
  var o = new Order(5, 2m) { Note = "rush" };
  return o.Note;                  // "rush"  (the body set "new order"; the initializer overrode it)
}
```

```osy title="default parameters and the implicit default constructor" test app=constructors
class Box {
  public int Size;
  public Box(int size = 3) { Size = size; }
}

int DefaultSize() { var b = new Box(); return b.Size; }   // 3  (the constructor's default)
int GivenSize()   { var b = new Box(7); return b.Size; }  // 7

// A class with no declared constructor keeps the plain build.
class Point { public int X; public int Y; }
int Origin() { var p = new Point { X = 0, Y = 0 }; return p.X + p.Y; }   // 0
```

### Several constructors, and chaining between them   {#overloads}
Declare as many as differ in their parameter types. `: this(…)` runs another of them first, which is how shared
setup stays in one place:

```osy title="three ways to make a Panel" test app=constructors
class Panel {
  public decimal Width;
  public decimal Height;

  public Panel(decimal width, decimal height) {   // the one that actually assigns
    Width = width;
    Height = height;
  }

  public Panel(decimal width) : this(width, 20m) { }   // chains to it
  public Panel() : this(10m) { }                       // …and so does this, one hop further
}
```

Each `new` picks by its arguments, and a chain runs all the way down before the chaining body:

```osy title="each new selects, and the chain completes" run app=constructors
[Test]
void Constructors_Select_And_Chain() {
  Assert.Equal(3m, new Panel(3m, 4m).Width);
  Assert.Equal(20m, new Panel(3m).Height);     // the default the one-arg constructor passed on
  Assert.Equal(10m, new Panel().Width);        // two hops: () → (decimal) → (decimal, decimal)
}
```

## See also       {#see-also}
- [class methods](https://osysharp.com/reference/class/methods/) — instance methods on a class (the same member-body mechanism the constructor reuses)
- [Typed locals](https://osysharp.com/reference/function/typed-locals/) — declaring the local that holds a constructed instance


---

<!-- https://osysharp.com/reference/config/index/ -->

# App configuration

> The cross-cutting settings an app declares in one place — the app config object. Two kinds live here: the external providers and credentials your app depends on (secrets, OAuth clients, an embedding provider) and the app-wide policies (data classifications, audit-trail access, an MCP tool surface). Each is one typed declaration.

<!-- id: config-index · area: config · stability: stable · html: https://osysharp.com/reference/config/index/ -->

## Summary        {#summary}
Some things belong to the **whole app**, not to any one entity or function: the API keys it needs, the OAuth providers
it signs users in with, the model it embeds text with, the sensitivity policy over its data. Those are declared once,
as typed properties on the app config object, so there is a single place to look and the compiler checks each one.

They fall into two groups: **what the app depends on** (secrets, OAuth clients, an embedding provider) and **policies
over the app** (classifications, audit access, an MCP surface).

## Description    {#description}

### What the app depends on   {#dependencies}
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — `app.Secrets` declares the named secrets the app uses (API keys, tokens, client secrets). Each
  is `new Secret("Name")`; a value is never inlined in source. Mark one `{ UserScoped = true }` for a per-user secret.
- [OAuth clients (app.OAuthClients)](https://osysharp.com/reference/config/oauth-clients/) — `app.OAuthClients` declares the third-party OAuth providers, both for signing users in
  and for calling an external API on a user's behalf.
- [embedding provider (app.Embedding)](https://osysharp.com/reference/config/embedding/) — `app.Embedding` names the embedding model that turns text into vectors, which is what powers
  semantic search over `[Searchable]` fields.
- [per-environment config (app.Config)](https://osysharp.com/reference/config/app-config/) — `app.Config` declares the app's per-environment settings (`new Setting("Name")`), read
  anywhere as `Config.Name`; each environment supplies its values from a checked-in `.env.<mode>` file. Non-secret
  configuration — the counterpart to `app.Secrets`.

### Policies over the app   {#policies}
- [data classifications (app.Classifications)](https://osysharp.com/reference/config/classifications/) — `app.Classifications` maps a data-sensitivity level (a `DataClass` — PII, Financial,
  Secret, …) to the `[Role]` members allowed to read fields marked at that level. You declare the mapping once; fields
  opt in with a classification attribute.
- [audit read access (app.Audit)](https://osysharp.com/reference/config/audit/) — `app.Audit` declares who may read the app's audit trail. The platform records entity changes
  automatically; this gates the reading of that record.
- [workflow run retention (app.Workflow)](https://osysharp.com/reference/config/workflow/) — `app.Workflow` declares how long FINISHED workflow runs are kept. Undeclared, a completed run
  is kept for ever with everything it owns; a window reaps it, and a run still in progress is never reaped whatever
  its age. It is also a ceiling on how long [audit read access (app.Audit)](https://osysharp.com/reference/config/audit/) can keep the workflow transition trail.
- [MCP tool server (app.McpServer)](https://osysharp.com/reference/config/mcp-server/) — `app.McpServer` exposes the app to an MCP client (an AI agent) as a set of tools, grouped
  into catalogs, each with its own visibility.

### UI surfaces the app owns   {#ui}
- [UI surfaces (app.Ui)](https://osysharp.com/reference/config/ui/) — `app.Ui` nominates the app's own components for the *system surfaces* the platform would otherwise
  draw a bare fallback for: `ConnectionSurface` (the server dropped — see [Connection](https://osysharp.com/reference/ui/connection/)), `NotFoundSurface` (404),
  `ForbiddenSurface` (403), and `ErrorSurface` (an unexpected load failure).

### One object, checked at compile time   {#compile-time}
Because each of these is a typed declaration rather than a config file parsed at boot, a missing provider, a
misspelled role, or a secret that no code reads is caught when the app compiles — not in production. The
[`use`](https://osysharp.com/reference/types/use/) declaration is the companion in the manifest: `use` brings a capability's tables and types into
the app; the config object here tunes how the app uses them.

## See also       {#see-also}
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) · [OAuth clients (app.OAuthClients)](https://osysharp.com/reference/config/oauth-clients/) · [embedding provider (app.Embedding)](https://osysharp.com/reference/config/embedding/) — the app's external dependencies
- [data classifications (app.Classifications)](https://osysharp.com/reference/config/classifications/) · [audit read access (app.Audit)](https://osysharp.com/reference/config/audit/) · [MCP tool server (app.McpServer)](https://osysharp.com/reference/config/mcp-server/) — the app-wide policies
- [use](https://osysharp.com/reference/types/use/) — declaring a capability the app depends on


---

<!-- https://osysharp.com/reference/config/mcp-server/ -->

# MCP tool server (app.McpServer)

> `app.McpServer` exposes your app to an MCP client (an AI agent) as a set of tools. Tools are grouped into named `Catalogs`, each gated by a `VisibleTo` policy (who may see it). A tool is one of: `new Tool(Function)` (call one of your functions), `new CrudTool<Entity>() { Operations = [...] }` (create/read/update/delete an entity), or `new Tool(Knowledge.Search)` (semantic search). A singleton.

<!-- id: config-mcp-server · area: config · stability: stable · html: https://osysharp.com/reference/config/mcp-server/ -->

## Summary        {#summary}
`app.McpServer` exposes your application to an MCP (Model Context Protocol) client — typically an AI agent — as a set of
**tools** the agent may call. As with the REST surface, you **expose what you already have** rather than write a tool
handler: a tool is one of your functions, an entity's CRUD operations, or a semantic search over your knowledge. Tools
are grouped into named **catalogs**, and each catalog is gated by a `VisibleTo` policy that decides which principals may
see and call it. `app.McpServer` is a **singleton** — an app has at most one MCP server.

```osy syntax
app.McpServer = new McpServer {
  Catalogs = [
    new ToolCatalog("admin-tools") {
      Description = "Admin-only tools",
      VisibleTo   = IsAdmin,
      Tools = [ new Tool(OrderTotal) ],
    },
  ],
};
```

## Signature      {#signature}
```osy syntax
app.McpServer = new McpServer {
  Catalogs = [                        // one entry per named group of tools
    new ToolCatalog("Name") {         // the catalog's name, shown to the client
      Description = "…",              // optional; describes the catalog to the agent
      VisibleTo   = IsAdmin,          // a declared `policy` — who may see this catalog
      Tools = [                       // the tools this catalog exposes
        new Tool(OrderTotal),                                    // call a function
        new CrudTool<Order>() { Operations = [CrudOp.Read] },    // CRUD over an entity
        new Tool(Knowledge.Search),                              // semantic search
      ],
    },
  ],
};
```

`app.McpServer` is a singleton; its `Catalogs` is a list — an app may group its tools into several independently-gated
catalogs.

## Description    {#description}
A `ToolCatalog` has:

- **`"Name"`** — the catalog's name, passed to the constructor, shown to the MCP client.
- **`Description`** *(optional)* — prose describing the catalog to the agent.
- **`VisibleTo`** — the name of a declared `policy`. The catalog (and every tool in it) is only visible and callable to
  principals for whom the policy holds. `VisibleTo` must name a **declared policy** — an undeclared name is a compile
  error that lists your policies. The policy is defined over your `[Principal]` and its `[Role]`, e.g.
  `policy IsAdmin => user.Role == AppRole.Admin;`.
- **`Tools`** — a list of tools. A tool is one of three kinds:
  - **`new Tool(Function)`** — exposes one of your functions as a callable tool. The function must be declared — an
    unknown name is a compile error that lists your functions.
  - **`new CrudTool<Entity>() { Operations = [CrudOp.…] }`** — exposes create/read/update/delete over one entity;
    `Operations` chooses which of `Create` / `Read` / `Update` / `Delete` are available. The entity must be defined in
    your app.
  - **`new Tool(Knowledge.Search)`** — exposes semantic (vector) search over your app's searchable knowledge.

A catalog is either a group of **CRUD/function tools** *or* a **polymorphic** tool group — a catalog mixes tool kinds
freely, but each `Tools` entry is exactly one tool. You write no tool schema, no argument parsing, and no dispatch: the
signature of the function and the shape of the entity are the contract.

## Examples       {#examples}
A complete app that exposes one function and one entity's full CRUD as an admin-only catalog. Note the catalog needs a
declared `[Principal]`, its `[Role]` enum, and the `policy` that `VisibleTo` references:

```osy title="basic" test app=config-mcp-example
entity Order { [Required, MaxLength(200)] string CustomerRef; decimal Total; }

[Role] enum AppRole { Staff, Admin }
[Principal] entity User {
  [MaxLength(200)] string Email;
  AppRole Role;
}

policy IsAdmin => user.Role == AppRole.Admin;

decimal OrderTotal(decimal subtotal, decimal tax) { return subtotal + tax; }

app.McpServer = new McpServer {
  Catalogs = [
    new ToolCatalog("admin-tools") {
      Description = "Admin-only tools",
      VisibleTo = IsAdmin,
      Tools = [
        new Tool(OrderTotal),
        new CrudTool<Order>() { Operations = [CrudOp.Create, CrudOp.Read, CrudOp.Update, CrudOp.Delete] },
      ],
    },
  ],
};
```

## See also       {#see-also}
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — the `[Principal]`, `[Role]`, and `policy` that `VisibleTo` gates a catalog with
- [publishing a REST API (app.Apis)](https://osysharp.com/reference/api/rest/) — `app.Apis`, the HTTP surface that exposes the same functions and entities to non-agent callers
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — the searchable knowledge that `new Tool(Knowledge.Search)` exposes


---

<!-- https://osysharp.com/reference/config/oauth-clients/ -->

# OAuth clients (app.OAuthClients)

> `app.OAuthClients` declares the third-party OAuth providers your app uses — for signing users in (Login) and for connecting to an external API on a user's behalf (Connection). Each is `new OAuthClient("Name")` naming a Provider (Google/Github/Microsoft/Oidc), the Capabilities, a ClientId, and a ClientSecret (a `Secret.X` handle). Referenced elsewhere by its `OAuthClient.Name` handle.

<!-- id: config-oauth-clients · area: config · stability: stable · html: https://osysharp.com/reference/config/oauth-clients/ -->

## Summary        {#summary}
`app.OAuthClients` declares the third-party OAuth providers your application talks to. There are two reasons to declare
one: to let users **sign in** with an external identity (Login), and to **connect** to an external API and act on a
user's behalf (Connection). Each client is a `new OAuthClient("Name")` that names a `Provider`, the `Capabilities` you
need, a `ClientId`, and a `ClientSecret` (a `Secret.X` handle). The client's `Name` is how the rest of your app refers
back to it as `OAuthClient.Name`.

```osy syntax
app.OAuthClients = [
  new OAuthClient("Google") {
    Provider     = OAuthProvider.Google,
    Capabilities = [OAuthCapability.Login],
    ClientId     = "your-client-id",
    ClientSecret = Secret.GoogleOAuth,
  },
];
```

## Signature      {#signature}
```osy
app.Secrets = [ new Secret("GoogleOAuth") ];   // the handle the client below reads

app.OAuthClients = [
  new OAuthClient("Name") {            // one entry per provider client
    Provider     = OAuthProvider.Google,          // Google | Github | Microsoft | Oidc
    Capabilities = [OAuthCapability.Login, OAuthCapability.Connection],
    ClientId     = "your-client-id",              // the provider-issued client id
    ClientSecret = Secret.GoogleOAuth,            // a Secret.X handle, never a literal
  },
];
```

`app.OAuthClients` is a list — an app may declare several clients, one per provider (or several against the same
provider for different capabilities).

## Description    {#description}
An `OAuthClient` has:

- **`Provider`** — an `OAuthProvider` enum member naming the identity provider: `Google`, `Github`, `Microsoft`, or
  `Oidc` (a generic OpenID Connect provider).
- **`Capabilities`** — a list of `OAuthCapability`. `Login` lets users sign into your app with this provider;
  `Connection` lets your app connect to the provider's API and act on a signed-in user's behalf. A client may declare
  both.
- **`ClientId`** — a string: the client identifier the provider issued when you registered your app with it.
- **`ClientSecret`** — a `Secret.X` handle referring to a secret declared in [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/). You never write the
  secret value inline; you reference the named secret.

The client's **`Name`** (the argument to `new OAuthClient("…")`) is its handle. That name is how the rest of your app
refers back to the client as `OAuthClient.Name` — for example an `OAuthAuth` method that signs users in via this
provider references the client by its `OAuthClient.Name` handle.

### The capability: `use Osysharp.Security.Oauth;`     {#capability}
Per-user OAuth links are a **capability**, `Osysharp.Security.Oauth`. Declaring an `OAuthClient` sets up the *provider*;
the capability adds the per-**user** side: a `UserOAuthLink` entity that ties one of your app's users to the identity
(and, for a `Connection` client, the stored tokens) they hold with a provider. The platform writes these links when a
user signs in or connects; the `use`/`using` gates whether your own code may name and query them.

- **`use Osysharp.Security.Oauth;`** in the `app { }` manifest declares the dependency.
- **`using Osysharp.Security.Oauth;`** at the top of a source file imports the `UserOAuthLink` name so a function or
  query in that file may reference a user's linked provider identities.

See [use](https://osysharp.com/reference/types/use/) for how `use` (the dependency) and `using` (the file-level import) differ.

## Examples       {#examples}
Declare the secret, then declare a Google client that lets users sign in:

```osy title="basic" test app=config-oauth-example
app.Secrets = [ new Secret("GoogleOAuth") ];

app.OAuthClients = [
  new OAuthClient("Google") {
    Provider = OAuthProvider.Google,
    Capabilities = [OAuthCapability.Login],
    ClientId = "your-client-id",
    ClientSecret = Secret.GoogleOAuth,
  },
];
```

## See also       {#see-also}
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — `app.Secrets`, where the `Secret.X` used by `ClientSecret` is declared
- [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/) — declaring an auth method that signs users in via an `OAuthClient`
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — granting the first user their role after an OAuth sign-in
- [use](https://osysharp.com/reference/types/use/) — `use Osysharp.Security.Oauth;` (the dependency) and `using Osysharp.Security.Oauth;` (the import)


---

<!-- https://osysharp.com/reference/config/ui/ -->

# UI surfaces (app.Ui)

> `app.Ui` nominates the app's own components for the "system surfaces" the platform would otherwise render a bare fallback for — the connection-loss overlay, the not-found (404) page, the forbidden (403) page, and the error page — and tunes the automatic busy indicator. A singleton; the `Ui` block groups these so more can be added without a new setting each time.

<!-- id: config-ui · area: config · stability: preview · html: https://osysharp.com/reference/config/ui/ -->

## Summary        {#summary}
`app.Ui` is where you hand the platform your **own** components for the *system surfaces* it would otherwise draw a
bare fallback for — the "something happened, here's the app's branded page" moments. It is a **singleton** — one UI
configuration per app — and it groups these overrides under one `Ui` block, so an app opts into each surface it wants to
own and inherits the platform default for the rest.

```osy syntax
app.Ui = new AppUi {
  ConnectionSurface = OfflineOverlay,   // shown when the server drops mid-session
  NotFoundSurface   = NotFoundPage,     // shown (HTTP 404) when no route matches the address
  ForbiddenSurface  = ForbiddenPage,    // shown when a signed-in user is refused a page (403)
  ErrorSurface      = ErrorPage,        // shown when a page fails to load unexpectedly
};
```

## Signature      {#signature}
```osy
app.Ui = new AppUi {
  ConnectionSurface = OfflineOverlay,   // the connection-loss overlay
  NotFoundSurface   = NotFoundPage,     // the not-found (404) page
  ForbiddenSurface  = ForbiddenPage,    // the forbidden (403) page
  ErrorSurface      = ErrorPage,        // the error page
};
```

`app.Ui` is a single value, not a list. Each member names a component **by name** — the same name you gave it in its
`component X { … }` declaration.

## Description    {#description}
An `AppUi` has these members (each optional — omit one, or omit `app.Ui` entirely, to keep the platform default):

- **`ConnectionSurface`** — the component the platform mounts, as a fixed overlay over the current page, when the
  browser loses its link to the server. It reads the [Connection](https://osysharp.com/reference/ui/connection/) ambient (`Connection.State`, `Connection.Attempts`)
  and calls `Connection.Retry()` / `Connection.Reload()`. Because it has to render **with the server gone**, it must be
  self-contained — built from the [component](https://osysharp.com/reference/ui/component/) built-in elements, with no child components to fetch and no data to
  load. See [Connection](https://osysharp.com/reference/ui/connection/) for the full surface and its constraints.
- **`NotFoundSurface`** — the page the platform serves, **with HTTP status 404**, when a visitor hits an address the app
  doesn't route. The server sends your app's shell with this component as the page (so the 404 keeps the correct status
  for crawlers and monitoring, but the *body* is your branded page instead of plain text) and the client renders it.
  Unlike the connection surface, a 404 means the server **answered**, so this page may use anything the app has. Mark it
  `[AllowAnonymous]` so a bad URL renders it for anyone rather than bouncing a signed-out visitor to login.
- **`ForbiddenSurface`** — the page shown when a **signed-in** user is refused a page they aren't permitted to see (a
  403). The platform mounts it in place of the built-in access-denied surface.
- **`ErrorSurface`** — the page shown when a page fails to load for an **unexpected** reason, in place of the built-in
  "couldn't load" surface.

Three more members tune the **busy indicator** — the automatic spinner the platform shows while an action is in flight
(see [Pending](https://osysharp.com/reference/ui/pending/) for the full surface, including the `Pending` ambient):

- **`PendingIndicator`** — your own component for the global busy affordance, in place of the built-in top progress bar.
- **`PendingDelayMs`** — how long an action must run before the indicator appears (so an instant action never flashes
  one); `0` uses the platform default.
- **`PendingMinShowMs`** — once shown, the minimum time the indicator stays up, so it can't blink off; `0` uses the
  default.

The forbidden and error surfaces render at *boot-failure* time — a refusal or a failure that can happen **before the app
has a working session, or while the server is unreliable**. So the platform **inlines their trees into the page** up
front, and the client renders them with no further request. That is what makes them work when nothing else does — and it
puts the same constraint on them as the connection surface: they must be **self-contained**, built from the
[component](https://osysharp.com/reference/ui/component/) built-in elements (with `[Composable]` children bundled), carrying no data. Offer a way out with plain
`Link`s (sign in as a different account, go home). Mark each `[AllowAnonymous]`.

Each nominated component must exist, and none is a routed page — the platform serves or mounts each by name for its
occasion, so none needs a `[Page("…")]`.

`app.Ui` is a **singleton**: an app declares it once. Removing a member reverts that surface to the platform default.

> Sign-in is deliberately **not** part of this block — it is a real *flow*, declared via
> `app.AuthBootstrap { LoginPage = … }`, not a passive surface.

## Examples       {#examples}
Declare the surfaces, then nominate them:

```osy test app=ui-connection-and-notfound-surfaces
[AllowAnonymous]
component OfflineOverlay() {
  action Retry() { Connection.Retry(); }
  render {
    if (Connection.State == ConnState.Lost) {
      Row(align: Align.Center, justify: Justify.Center, position: Position.Fixed, inset: 0, bg: "#1a1a1a") {
        Text("Can't reach the server");
      }
    }
  }
}

[AllowAnonymous]
component NotFoundPage() {
  render {
    Row(align: Align.Center, justify: Justify.Center, minH: "100vh", bg: Colors.Surface) {
      Stack(gap: 3, align: Align.Center) {
        Text("Page not found");
        Link(href: "/") { Text("Go home"); }
      }
    }
  }
}

app.Ui = new AppUi {
  ConnectionSurface = OfflineOverlay,
  NotFoundSurface   = NotFoundPage,
};
```

## See also       {#see-also}
- [Connection](https://osysharp.com/reference/ui/connection/) — the `Connection` ambient the connection surface reads, and its offline-render constraint.
- [Pending](https://osysharp.com/reference/ui/pending/) — the automatic busy indicator the `Pending*` members tune, and the `Pending` ambient.
- [component](https://osysharp.com/reference/ui/component/) — components, `render` blocks, and the built-in elements a surface is built from.


---

<!-- https://osysharp.com/reference/config/audit/ -->

# audit read access (app.Audit)

> `app.Audit` configures the app's audit trails — WHO may read each one, and whether it is recorded at all. The platform keeps six trails on by default (entity changes, classified reads, workflow transitions, sign-ins, LLM calls, schedule occurrences); each read-surface (e.g. `EntityAuditRecord`) maps to an `AuditSurface` whose `Read = user => <predicate>` compiles to a deny-by-default read policy, and whose `Enabled = false` turns that trail off (audit is the developer's choice).

<!-- id: config-audit · area: config · stability: stable · html: https://osysharp.com/reference/config/audit/ -->

## Summary        {#summary}
`app.Audit` configures your app's **audit trails** — the records the platform keeps of what happened. You never write
to a trail; `app.Audit` does two things: it gates **read** access (who may see a trail) and controls **capture**
(whether a trail is recorded at all). Each read-surface — such as `EntityAuditRecord`, the per-entity change record —
maps to a `new AuditSurface { … }` carrying an optional `Read = user => <predicate>` (a boolean over the `[Principal]`
bound as `user`) and an optional `Enabled = <bool>`. Absent an `app.Audit` entry a surface is **on** (audit is on by
default) and **deny-by-default for reads** (no one may read it until a `Read` predicate opens it).

```osy syntax
app.Audit = new AuditConfig {
  EntityAuditRecord = new AuditSurface { Read = user => user.IsAuditor },
  WorkflowAuditRecord = new AuditSurface { Enabled = false },   // turn a trail off
};
```

## Signature      {#signature}
```osy
app.Audit = new AuditConfig {
  EntityAuditRecord = new AuditSurface {          // one entry per surface
    Read      = user => user.IsAuditor,           // (optional) who may read the trail — predicate over the [Principal]
    Enabled   = true,                             // (optional) whether the trail is recorded — default true
    Retention = TimeSpan.FromDays(365),           // (optional) how long rows are kept — default forever
  },
};
```

`app.Audit` is a single `AuditConfig`. Each named surface (e.g. `EntityAuditRecord`) is an `AuditSurface` whose `Read`
decides who may read that trail, `Enabled` decides whether it is captured, and `Retention` decides how long its rows are
kept. All three members are optional; a surface with none is the default (on, closed to reads, kept forever).

## Description    {#description}
The audit trails are **not something you build** — the platform records them on its own. There are six, each a named
read-surface: **`EntityAuditRecord`** (entity create/update/delete), **`AccessAuditRecord`** (reads of classified data),
**`WorkflowAuditRecord`** (workflow transitions), **`AuthAuditRecord`** (sign-in events), **`LlmCallRecord`** (LLM
calls — model, tokens, cost, and prompt/response), and **`ScheduleOccurrenceRecord`** (what a schedule did, or did not
do, each time it came due). `app.Audit` configures them:

- **`AuditConfig`** — the top-level value assigned to `app.Audit`. It holds one entry per surface you want to configure,
  plus the app-wide `Redact` policy.
- **`AuditSurface`** — a surface's config, carrying two optional members:
  - **`Read = user => <predicate>`** — who may read the trail. The predicate **binds the `[Principal]` as `user`** and
    **must be a boolean** over its fields (e.g. `user.IsAuditor`); a field it reads must exist on the `[Principal]`, or
    it is a compile error naming what the principal does define. It lowers to a **deny-by-default read policy plus an
    `allow read when <predicate>` rule**, enforced by the **same security runtime as every other entity** — no separate
    audit-permission path. A surface you never give a `Read` stays closed; one you do opens only to accepting principals.
  - **`Enabled = <bool>`** — whether the trail is captured at all. See [below](#enabled).
  - **`Retention = <TimeSpan>`** — how long rows are kept before the platform purges them. See [below](#retention).

### Turning a trail off: `Enabled`     {#enabled}
Every trail is **on by default**. `Enabled = false` on a surface turns that one off — the platform stops recording it:

```osy syntax
app.Audit = new AuditConfig {
  WorkflowAuditRecord = new AuditSurface { Enabled = false },   // don't record workflow transitions
  AuthAuditRecord     = new AuditSurface { Enabled = false },   // don't record sign-ins
};
```

- **Per-surface.** Each surface is switched independently; omit a surface (or set `Enabled = true`) to keep
  it on. Disabling `EntityAuditRecord` turns off the **whole** entity-change trail; for finer control, exclude a single
  entity with the `[Audit(None)]` attribute on its declaration.
- **`Read` and `Enabled` are independent.** `Read` governs who may see an existing trail; `Enabled` governs whether the
  trail is written. They can be combined (`new AuditSurface { Read = user => user.IsAuditor, Enabled = false }`) or used
  alone.
- **Auditing is the developer's choice** — including the sign-in trail. Turning `AuthAuditRecord` off is a declared
  decision not to capture sign-ins; it does not weaken the guarantee that, **while on**, the trail is complete and
  tamper-proof (host-written, read-only to your app).

### Redaction: what never appears in the trail     {#redaction}
The audit trail records **entity changes** — for each changed property, its old and new value. Some values must never
be captured: a credential (`PasswordHash`, `ResetToken`) or a `[Classification]`-restricted field would otherwise land,
**unmasked**, in a durable, exportable log. `app.Audit.Redact` declares — **explicitly** — what to hold back:

- **`Redact = new AuditRedaction { … }`** — the app-wide redaction policy. Optional; declare it once.
- **`Properties = [Entity.Prop, …]`** — these exact properties are redacted wherever an audit record would capture
  their value.
- **`Classifications = [Enum.Member, …]`** — any property carrying one of these `[Classification]` levels is redacted.
  This names the **same** classification levels the app declares for read-masking, so audit-redaction and read-masking
  cannot drift.

A redacted property **still appears** in the change record — the "it changed" signal is preserved — but its old and new
values are replaced with the marker `‹redacted›`, never the raw value. Redaction is **declared, never inferred**: a
property is redacted only if named here (directly, or via its classification). An app that declares no `Redact` captures
values verbatim.

The **same one `Redact` policy also governs the LLM-call trail**. A classified value your app is allowed to read may
legitimately go into a prompt sent to the model — but if that value's property is covered by `Redact`, it must not be
persisted in the `LlmCallRecord` body. There is nothing extra to declare: the property's `[Classification]` (or its name
in `Redact`) is the whole instruction. The model still receives the value; the retained log does not. Precise
value-level redaction lands as the platform gains typed visibility into what an agent feeds a model; until then a call
known to carry such a value is logged **metrics-only** (tokens/cost/timing kept, prompt/response body dropped) — never a
partial leak.

### How long rows are kept: `Retention`     {#retention}
By default a trail's rows are kept **forever**. `Retention = <TimeSpan>` sets a window; the platform periodically purges
that surface's rows older than the window. Use the standard C# `TimeSpan` factories:

```osy syntax
app.Audit = new AuditConfig {
  EntityAuditRecord = new AuditSurface { Retention = TimeSpan.FromDays(365) },   // keep a year of changes
  AccessAuditRecord = new AuditSurface { Retention = TimeSpan.FromDays(90) },    // keep 90 days of access logs
};
```

- **Per-surface, constant.** Each surface has its own window; a surface with no `Retention` is kept forever. The window
  is a compile-time constant — `TimeSpan.FromDays(…)` / `FromHours(…)` / `FromMinutes(…)` / `FromSeconds(…)` over a
  literal (not a per-row expression).
- **Enforced by a platform sweep.** A platform-owned background job runs periodically (per app) and deletes each
  surface's expired rows — you declare the window; the platform does the deleting. It is best-effort and eventually
  consistent: rows are removed on the next sweep after they age out, not at the exact instant.

#### Two horizons for the LLM trail: `ContentRetention`     {#llm-retention}
The `LlmCallRecord` trail is special: each row holds a fat **body** (the prompt, response, and tool definitions) *and*
billing-grade **metrics** (tokens, cost, provider, model). Those want opposite lifetimes — the body is sensitive and
should expire quickly, the cost record is a financial fact you keep for reporting. So the LLM surface takes **two**
windows:

```osy syntax
app.Audit = new AuditConfig {
  LlmCallRecord = new AuditSurface {
    ContentRetention = TimeSpan.FromDays(30),    // the BODY (prompt/response) is cleared after 30 days
    Retention        = TimeSpan.FromDays(365),   // the whole record (incl. cost) is deleted after a year
  }
};
```

- **`ContentRetention`** — how long the prompt/response **body** is kept. Past it, the sweep **nulls the body columns**
  but keeps the row and all its metrics/cost. This member is **LLM-only** (no other surface has a body-vs-metrics split;
  declaring it elsewhere is a compile error).
- **`Retention`** — how long the **record** is kept, exactly as for the other surfaces; past it the whole row is
  deleted. For `LlmCallRecord` this is the metrics/cost horizon, and should be ≥ `ContentRetention`.
- Omit `ContentRetention` and the body lives as long as the record; omit `Retention` and the metrics are kept forever
  (bodies still expire on `ContentRetention` if set).

### System-access (sign-in) trail: `AuthAuditRecord`     {#auth-trail}
Beyond entity changes, the platform records **system-access events** — who logged in, who FAILED to, and who logged
out — on the `AuthAuditRecord` surface. Every sign-in path routes its outcome through one funnel that writes the trail,
so it is complete across providers by construction: the platform-managed password and OAuth flows, and an app's OWN
sign-in (an `[AuthMethod]` function that calls `Security.IssueJwt`) — a successful `Security.IssueJwt` records a
`LoginSucceeded` for you. Each row carries the `Event`
(`LoginSucceeded` / `LoginFailed` / `SignupSucceeded` / `Logout`), the `AttemptedLogin` (the username tried — the
brute-force / account-enumeration signal, recorded even for a failed unknown-user attempt), the `AuthMethod`, a
`FailureReason` on a failure, and the client info. Like every trail it is **host-written and read-only** to your app,
gated by `app.Audit.AuthAuditRecord.Read`, and — like every trail — subject to `Enabled` (see [above](#enabled)): an app
may decline to record sign-ins with `AuthAuditRecord = new AuditSurface { Enabled = false }`.

### LLM-call trail: `LlmCallRecord`     {#llm-trail}
Every call your app makes to a language model is recorded on the `LlmCallRecord` surface — the model and provider, the
token counts (input / output / cache), the computed **cost**, the timing, and (when logging is on) the prompt and
response. It is the per-call ledger behind "what did this app spend on which model." Like every trail it is
**host-written and read-only** to your app, gated by `app.Audit.LlmCallRecord.Read`, and switched by `Enabled`:

```osy syntax
app.Audit = new AuditConfig {
  LlmCallRecord = new AuditSurface { Read = user => user.IsAuditor, Enabled = true },
};
```

- **`Enabled = false` turns the whole per-call trail off** — no call rows are written. It does **not** affect the LLM
  budget: your daily token/cost limits are still enforced and rolled up (spend control is independent of the audit log).
- **The record is queryable** with `using Osysharp.Llm.Observability;` (its own capability), the same way the other
  trails open with `using Osysharp.Observability;` — see [below](#capability).

### Schedule-occurrence log: `ScheduleOccurrenceRecord`     {#occurrence-trail}
A [Schedule (recurring work)](https://osysharp.com/reference/scheduling/schedule/) produces work on a cadence whether or not anyone is watching, so the question people ask it
is usually about the times it produced **nothing**. The `ScheduleOccurrenceRecord` surface answers that: one row for
every occurrence the platform considered, carrying when it was `DueAt`, when the platform looked (`At`), the `Outcome`
(`Produced` / `Skipped` / `Retired` / `UnknownTarget`), the row it produced, and `CoalescedCount` — how many further
occurrences a catch-up absorbed.

Every consideration is recorded, **the ordinary ones included**, and that is what gives an absent row a meaning: no
record for last night means the platform never looked, which is a different fault from a night that was skipped. A
trail holding only the exceptions leaves those two indistinguishable.

Unlike the other five, this trail's producer is a **cadence rather than a person** — a minutely schedule writes some
half a million rows a year — so it is the one most worth a `Retention` window:

```osy syntax
app.Audit = new AuditConfig {
  ScheduleOccurrenceRecord = new AuditSurface {
    Read      = user => user.IsOnCall,
    Retention = TimeSpan.FromDays(90),
  }
};
```

### The capability: `use Osysharp.Observability;`     {#capability}
The audit trail is a **capability**, `Osysharp.Observability`. Its schema is always present — the platform records
`EntityAuditRecord` (entity create/update/delete), `AccessAuditRecord` (reads of classified data), and
`AuthAuditRecord` (sign-in events) whether or not your app opts in. The `use`/`using` for `Osysharp.Observability` gates
one thing: whether your own code may **name** the audit records to query them.

- **`use Osysharp.Observability;`** in the `app { }` manifest declares the dependency.
- **`using Osysharp.Observability;`** at the top of a source file imports the audit read-surface names, so a function or
  query in that file may reference `EntityAuditRecord` / `AccessAuditRecord` / `AuthAuditRecord`.

`ScheduleOccurrenceRecord` is the exception that needs no `using` at all — a schedule is core rather than a
capability, so its occurrence log is in scope wherever you write a query.

`app.Audit` (above) then decides **who** may read what you thus reference — the capability opens the names to your
code; the `Read` predicates open the rows to a principal. See [use](https://osysharp.com/reference/types/use/) for how `use` and `using` differ.

## Examples       {#examples}
An app whose audit trail is readable only by staff flagged as auditors. The `[Principal]` declares the `IsAuditor`
field the predicate reads:

```osy title="basic" test app=config-audit-example
[Principal] entity Staff {
  [Required, MaxLength(200)] string Name;
  bool IsAuditor;
}

app.Audit = new AuditConfig {
  EntityAuditRecord = new AuditSurface { Read = user => user.IsAuditor }
};
```

Redaction alongside the read gate: the account's password hash (by its `Secret` classification) and its API key (named
directly) are recorded as `‹redacted›`, never verbatim, while ordinary fields are captured as-is:

```osy title="redaction" test app=config-audit-redaction
enum DataClass { Public, Secret }

[Principal] entity Staff {
  [Required, MaxLength(200)] string Name;
  bool IsAuditor;
}

entity Account {
  [Required, MaxLength(200)] string Login;
  [MaxLength(200)] [Classification(DataClass.Secret)] string PasswordHash;
  string ApiKey;
}

app.Audit = new AuditConfig {
  EntityAuditRecord = new AuditSurface { Read = user => user.IsAuditor },
  Redact = new AuditRedaction {
    Properties      = [Account.ApiKey],       // this exact property
    Classifications = [DataClass.Secret]      // any property at the Secret level (Account.PasswordHash)
  }
};
```

Turning trails off is per-surface and independent of the read gate. Here workflow-transition auditing is disabled
outright, while the entity-change trail stays on but readable only by auditors:

```osy title="enabled" test app=config-audit-enabled
[Principal] entity Staff {
  [Required, MaxLength(200)] string Name;
  bool IsAuditor;
}

app.Audit = new AuditConfig {
  EntityAuditRecord   = new AuditSurface { Read = user => user.IsAuditor },
  WorkflowAuditRecord = new AuditSurface { Enabled = false }
};
```

Retention windows keep the trail bounded — a year of entity changes, ninety days of access logs, sign-ins forever:

```osy title="retention" test app=config-audit-retention
[Principal] entity Staff {
  [Required, MaxLength(200)] string Name;
  bool IsAuditor;
}

app.Audit = new AuditConfig {
  EntityAuditRecord = new AuditSurface { Read = user => user.IsAuditor, Retention = TimeSpan.FromDays(365) },
  AccessAuditRecord = new AuditSurface { Retention = TimeSpan.FromDays(90) }
  // AuthAuditRecord has no Retention → sign-ins are kept forever
};
```

## See also       {#see-also}
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — the `user => …` predicate form, and the `[Principal]` it binds
- [security { }](https://osysharp.com/reference/security/entity-security/) — the deny-by-default read policy + `allow read when` rule this lowers to
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — granting a principal the role a predicate can test
- [use](https://osysharp.com/reference/types/use/) — `use Osysharp.Observability;` (the dependency) and `using Osysharp.Observability;` (the import)
- [Schedule (recurring work)](https://osysharp.com/reference/scheduling/schedule/) — the recurring producer behind the `ScheduleOccurrenceRecord` trail


---

<!-- https://osysharp.com/reference/config/classifications/ -->

# data classifications (app.Classifications)

> Classifications map a data-sensitivity level (a `DataClass` — PII, Financial, Secret, …) to the `[Role]` members allowed to read fields marked at that level. Declared inside the bare `app = new() { … }` config object. A field marked `[Classification(DataClass.PII)]` is readable only by a principal holding one of the roles listed for PII.

<!-- id: config-classifications · area: config · stability: stable · html: https://osysharp.com/reference/config/classifications/ -->

## Summary        {#summary}
`app.Classifications` maps each **data-sensitivity level** to the set of `[Role]` members permitted to read fields
marked at that level. A level is a `DataClass` value — `PII`, `Financial`, `Secret`, and so on — and each
`Classification` pairs one level with a `Roles = [ … ]` list. A field annotated `[Classification(DataClass.PII)]` is
then readable only by a principal holding one of the roles listed for PII; to everyone else the field is masked. The
list is declared inside the bare `app = new() { … }` config object.

```osy syntax
enum DataClass { PII, Financial }

app = new() {
  Name = "Orders",
  Classifications = [
    new Classification(DataClass.PII)       { Roles = [Role.Staff, Role.Admin] },
    new Classification(DataClass.Financial) { Roles = [Role.Admin] },
  ],
};
```

## Signature      {#signature}
```osy
[Role] enum Role { Staff, Admin }        // the [Role] enum must be declared
enum DataClass { PII, Financial }        // …and the level vocabulary is an enum too

app = new() {
  Name = "Orders",
  Classifications = [                     // one entry per data-sensitivity level
    new Classification(DataClass.PII) {    // the level this entry governs
      Roles = [Role.Staff, Role.Admin],    // roles allowed to read fields at this level
    },
    new Classification(DataClass.Financial) { Roles = [Role.Admin] },
  ],
};
```

`Classifications` is a list — an app may map several independent `DataClass` levels, each to its own set of roles.

## Description    {#description}
Each `Classification` has:

- **`DataClass.<Level>`** — the constructor argument names the sensitivity level this entry governs (`PII`,
  `Financial`, `Secret`, …). One entry per level. The level vocabulary is an **enum the app declares** — it need not
  be called `DataClass`, and naming a member no enum declares is a compile error, exactly as it is in the
  `[Classification]` attribute.
- **`Roles`** — a list of `[Role]` enum members. A principal must hold **one of** these roles to read a field marked
  at this level. A principal holding none of them sees the field masked.

A field opts into a level with the **`[Classification]`** attribute — `[Classification(DataClass.<Level>)]` on the
member. The classification declared here is what that attribute resolves against: it decides which roles can read the
field. Because `Roles` references `[Role]`
members, the **`[Role]` enum must be declared** in the app — a role name that doesn't resolve is a compile error that
names the roles you do define.

The whole list lives inside the bare `app = new() { Name = "…", Classifications = [ … ] }` config object alongside the
app's other configuration.

## Examples       {#examples}
An app that classifies `PII` fields as readable by staff or admins, and `Financial` fields as admin-only. The `[Role]`
enum is declared because `Roles` references its members:

```osy title="basic" test app=config-classifications-example
[Role] enum Role { Staff, Admin }
enum DataClass { PII, Financial }

app = new() {
  Name = "Orders",
  Classifications = [
    new Classification(DataClass.PII)       { Roles = [Role.Staff, Role.Admin] },
    new Classification(DataClass.Financial) { Roles = [Role.Admin] },
  ],
};
```

## See also       {#see-also}
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — declaring the `[Role]` enum whose members appear in `Roles`
- [security { }](https://osysharp.com/reference/security/entity-security/) — entity-level read/write gates that compose with field classifications
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — the principal whose roles are checked against a field's classification


---

<!-- https://osysharp.com/reference/config/secrets/ -->

# declaring secrets (app.Secrets)

> `app.Secrets` declares the named secrets your app uses — API keys, tokens, client secrets. Each is `new Secret("Name")`, optionally `{ UserScoped = true }` for a per-user secret rather than one app-wide value. Everything else references a secret by its `Secret.Name` handle — `app.DefaultModel`'s `ApiKey`, an OAuth client's `ClientSecret`. A secret's VALUE is never in source, only its name; the value lives in the secret store.

<!-- id: config-secrets · area: config · stability: stable · html: https://osysharp.com/reference/config/secrets/ -->

## Summary        {#summary}
`app.Secrets` declares the named secrets your application **consumes** — a provider's API key, an access token, an
OAuth client secret. You declare a secret's **name** here; you never write its **value** in source.

⚠ **Not the API key of an API your app PUBLISHES.** That is a different direction and a different mechanism: a
published `app.Apis` route gated `Auth = new ApiAuth { ApiKey = true }` is authenticated by a **per-user** key kept on
the app's `[Principal]` row, and nothing in `ApiAuth` names a `Secret`. See [publishing a REST API (app.Apis)](https://osysharp.com/reference/api/rest/).

Each secret is `new Secret("Name")`, and optionally `{ UserScoped = true }` to make it a per-user secret rather than a
single app-wide value. Every other config slot that needs a secret refers to it by its `Secret.Name` handle rather than
by an inline string.

```osy syntax
app.Secrets = [
  new Secret("OpenAI"),
  new Secret("PersonalToken") { UserScoped = true },
];
```

## Signature      {#signature}
```osy
app.Secrets = [                              // one entry per named secret
  new Secret("Anthropic"),                            // app-wide: one value for the whole app
  new Secret("CalendarToken") { UserScoped = true },  // per-user: each user supplies their own value
];

// referenced elsewhere by handle, never by literal value:
app.DefaultModel = new LlmConfig { ApiKey = Secret.Anthropic };
```

`app.Secrets` is a list — an app may declare as many named secrets as it needs.

## Description    {#description}
Each entry is a `new Secret("Name")`, where the name is a **string literal**. That name is the only thing that lives in
source. The secret's actual value — the key, token, or password — is never written in your app; it lives in the secret
store and is supplied separately. On your own machine you supply it with `osy secret set`
([Giving a secret its value](https://osysharp.com/reference/local/giving-a-secret-its-value/)); a deployed app's values are supplied by whoever operates its platform.

- **`new Secret("Name")`** — declares an **app-wide** secret. There is one value, shared by the whole application, used
  for every request regardless of who makes it (for example, one server-side API key for a provider).
- **`{ UserScoped = true }`** — makes the secret **per-user** instead. Every user of the app supplies their own value,
  and the secret resolves to the value belonging to the current user. Use this when the credential belongs to the
  person, not the app (for example, a user's personal access token).

Once declared, a secret is referenced everywhere else by its **`Secret.Name` handle** — not by re-typing the name as a
string and never by the value. `app.DefaultModel`'s `ApiKey`, an OAuth client's `ClientSecret`, and a REST API's key all
take a `Secret.Name` handle. The handle is how the platform links a config slot to the stored value at runtime while
keeping the value itself out of your source.

## Examples       {#examples}
Declare an app-wide secret and reference it from the default model's `ApiKey`:

```osy title="basic" test app=config-secrets-example
app.Secrets = [ new Secret("OpenAI") ];

app.DefaultModel = new LlmConfig {
  Provider = LlmProvider.OpenAI,
  Model    = "gpt-4o",
  ApiKey   = Secret.OpenAI,
};
```

A per-user secret — each user supplies their own value:

```osy title="user-scoped" test app=config-secrets-userscoped
app.Secrets = [ new Secret("PersonalToken") { UserScoped = true } ];
```

## See also       {#see-also}
- [reading a secret's value (Secret.Name)](https://osysharp.com/reference/function/secret-read/) — reading a declared secret's VALUE inside a function body
- [Giving a secret its value](https://osysharp.com/reference/local/giving-a-secret-its-value/) — how a declared secret gets its value on your machine
- [OAuth clients (app.OAuthClients)](https://osysharp.com/reference/config/oauth-clients/) — an OAuth client's `ClientSecret` is a `Secret.Name` handle
- [default LLM model (app.DefaultModel)](https://osysharp.com/reference/agent/default-model/) — `app.DefaultModel`, whose `ApiKey` references a declared secret
- [publishing a REST API (app.Apis)](https://osysharp.com/reference/api/rest/) — `app.Apis`. ⚠ A REST API's key is NOT one of these: `ApiAuth` names no `Secret`, and a
  published API's key is a per-user credential kept on the `[Principal]` row


---

<!-- https://osysharp.com/reference/config/embedding/ -->

# embedding provider (app.Embedding)

> `app.Embedding` declares the embedding model the app uses to turn text into vectors for semantic search over `[Searchable]` fields (see `Memory.Search`). You name a `Provider`, a `Model` string, the `ApiKey` (a `Secret.X` handle), and the vector `Dimensions` — plus an optional `BaseUrl` to run the model at an endpoint you choose. A singleton.

<!-- id: config-embedding · area: config · stability: stable · html: https://osysharp.com/reference/config/embedding/ -->

## Summary        {#summary}
`app.Embedding` declares the embedding model your application uses to turn text into vectors, so that semantic search
over `[Searchable]` fields (queried through `Memory.Search`) has something to embed against. You name a **provider**, a
**model**, the **secret** that authenticates to that provider, and the **dimensions** of the vectors it returns. It is a
**singleton** — one embedding configuration per app.

```osy syntax
app.Embedding = new EmbeddingConfig {
  Provider   = EmbeddingProvider.OpenAI,
  Model      = "text-embedding-3-small",
  ApiKey     = Secret.OpenAI,
  Dimensions = 1536,
};
```

## Signature      {#signature}
```osy
app.Secrets = [ new Secret("OpenAI") ];   // the handle the config below reads

app.Embedding = new EmbeddingConfig {
  Provider   = EmbeddingProvider.OpenAI,   // which embedding provider
  Model      = "text-embedding-3-small",   // the provider's embedding model id
  ApiKey     = Secret.OpenAI,              // a Secret.X handle from app.Secrets
  Dimensions = 1536,                       // the width of the produced vectors
  BaseUrl    = "https://…/v1/embeddings",  // optional — where to run it (default: the provider's own endpoint)
};
```

`app.Embedding` is a single value, not a list — an app configures exactly one embedding model.

## Description    {#description}
An `EmbeddingConfig` has five members, four of them required in practice:

- **`Provider`** — an `EmbeddingProvider` enum member naming which service produces the vectors. The example uses
  `EmbeddingProvider.OpenAI`.
- **`Model`** — the provider's embedding model id, as a string (for example `"text-embedding-3-small"`). This chooses
  which model the provider runs.
- **`ApiKey`** — a `Secret.X` handle referencing a secret declared in `app.Secrets`. It authenticates calls to the
  provider; the value itself lives outside your source.
- **`Dimensions`** — the width of each produced vector (for example `1536`). This must match the vector size the chosen
  model emits, so that stored `[Searchable]` vectors and query vectors are comparable.
- **`BaseUrl`** — optional. The endpoint that actually computes the embeddings. Omit it and the provider's own
  endpoint is used; give it and the model runs wherever you say — a service you host, a regional deployment, an
  inference endpoint inside a particular jurisdiction. It is the whole URL, not a host to append a path to, because a
  compatible service may mount the protocol wherever it likes.

`Provider` takes one of three values:

| Provider | What it means |
|---|---|
| `EmbeddingProvider.OpenAI` | OpenAI's own embeddings endpoint. `BaseUrl` optional (a proxy in front of it). |
| `EmbeddingProvider.Google` | Google's embeddings. |
| `EmbeddingProvider.OpenAICompatible` | Any service speaking the OpenAI embeddings protocol. **`BaseUrl` required** — the provider means "that protocol, at an endpoint you name", so there is no sensible default. |

> **The platform ships no embedding model of its own, on purpose.** A vector column is sized for the model that
> fills it, and vectors from one model are never comparable with another's — so an app that embedded with a small
> bundled model in development and a hosted one in production was never testing the feature it shipped. Declare one
> embedder and use it everywhere: locally, in tests, in production. It costs nothing to keep it free and offline:
> run an OpenAI-protocol embedding server on your own machine or in your own network and point
> `EmbeddingProvider.OpenAICompatible` at it — the second example below is exactly that.

**Why `BaseUrl` matters even when you are using OpenAI.** Where a model runs can be a requirement rather than a
preference — data-residency obligations are usually written about *where the data goes*, and embedding is the
operation that reads every indexed row. [default LLM model (app.DefaultModel)](https://osysharp.com/reference/agent/default-model/) takes a `BaseUrl` for the same reason — chat and
embeddings answer the same placement question, and an app that must place one almost always has to place both.

`app.Embedding` is a **singleton**: an application declares one embedding configuration, and every `[Searchable]` field
and every `Memory.Search` query uses it.

## Examples       {#examples}
Declare the secret, then configure OpenAI embeddings against it:

```osy title="basic" test app=config-embedding-example
app.Secrets = [ new Secret("OpenAI") ];

app.Embedding = new EmbeddingConfig {
  Provider = EmbeddingProvider.OpenAI,
  Model = "text-embedding-3-small",
  ApiKey = Secret.OpenAI,
  Dimensions = 1536,
};
```

Place the model at your own endpoint — the same declaration, with the provider and endpoint changed:

```osy title="an embedding model you host" test app=config-embedding-placed
app.Secrets = [ new Secret("Embeddings") ];

app.Embedding = new EmbeddingConfig {
  Provider   = EmbeddingProvider.OpenAICompatible,
  Model      = "bge-m3",
  ApiKey     = Secret.Embeddings,
  Dimensions = 1024,
  BaseUrl    = "https://embeddings.internal.example/v1/embeddings",
};
```

## See also       {#see-also}
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — `app.Secrets`, where the `ApiKey` handle referenced by `Embedding` is declared
- [[Searchable]](https://osysharp.com/reference/memory/searchable/) — marking a field `[Searchable]` so its text is embedded for search
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — `Memory.Search`, the query that runs against the embedded vectors
- [default LLM model (app.DefaultModel)](https://osysharp.com/reference/agent/default-model/) — `app.DefaultModel`, the same placement question for the chat model


---

<!-- https://osysharp.com/reference/config/memory/ -->

# identifier patterns (app.Memory)

> `app.Memory` tells search what an identifier looks like in YOUR data — an order number, a part code, an SKU. Search already spots identifier-shaped tokens in a query and matches them exactly, but it has to guess at the shape; declaring the patterns replaces the guess with an answer, and turns the guessing off.

<!-- id: config-memory · area: config · stability: stable · html: https://osysharp.com/reference/config/memory/ -->

## Summary        {#summary}
Some tokens carry meaning no amount of language understanding can recover: `ORD-004471`, `AB1234X`, `000346`. A
search engine that only understands *meaning* cannot find them reliably, so the platform also matches such tokens
EXACTLY, and weights that match heavily — but only when it is confident the query contains one.

Left alone it decides by shape: a token with a digit and at least four characters. That works, and it is a guess
about your data. `app.Memory` lets you state the shapes instead.

## Signature      {#signature}
```osy syntax
app.Memory = new MemoryConfig {
  Identifiers = [ @"ORD-\d{6}", @"[A-Z]{2}\d{4}[A-Z]" ]
};
```

## Description    {#description}
Each entry is a regular expression. A query is scanned for them, and anything found is matched exactly against the
corpus alongside the ordinary meaning-based search.

**Declaring turns the built-in guessing OFF.** You have answered the question the guess was standing in for, and
running both would put the guesses back on exactly the queries your patterns did not match — the ones you have
implicitly said contain no identifier.

That matters because the shape guess is not perfect on real language. Measured against a public benchmark, it fired
on 6 of 470 questions and five of those were not identifiers at all — `15th`, `10th`, `5-day`, `pre-1920`. Those
now abstain, but a corpus whose codes look like ordinary numbers is still better served by saying so.

**Patterns are checked when you compile.** A pattern that is not a valid regular expression is a compile error
naming it — not a search that fails later, in production, on the first query that happens to reach it.

**They are also checked for SPEED.** Patterns run once per query against text a caller supplied, so a pattern that
can take exponential time on an unlucky input is refused at compile time rather than becoming a way to stall your
app. In practice this rules out backreferences and lookaround; ordinary character classes, quantifiers and anchors
are all fine.

Use `@"…"` for a pattern, as in every example here. Inside `@"…"` a backslash is just a backslash, which is what a
regular expression is made of.

**Removing the declaration puts you back to the built-in behaviour** — the stored patterns go with it.

## Examples       {#examples}
```osy title="declare the shapes your data uses" test app=memory-identifiers
using Osysharp.Memory;

entity Order {
  [MaxLength(64)] string Reference;
  [Searchable(Memory)] string? Notes;
}

app.Memory = new MemoryConfig {
  Identifiers = [ @"ORD-\d{6}", @"[A-Z]{3}-\d{4}" ]
};
```

```osy title="a query naming one is matched exactly, as well as by meaning" test app=memory-identifiers
using Osysharp.Memory;

List<SearchHit> Chase(string question) {
  // "did we ever sort out the packaging fault on ORD-004471?" matches that order's notes on the token
  // itself — not merely on sounding like a packaging complaint.
  return Memory.Search(question, limit: 5);
}
```

## See also       {#see-also}
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — the search this configures
- [How retrieval works](https://osysharp.com/reference/memory/how-retrieval-works/) — why an exact token match is treated differently from a meaning match
- [per-environment config (app.Config)](https://osysharp.com/reference/config/app-config/) — the rest of the `app.` configuration surface


---

<!-- https://osysharp.com/reference/config/app-config/ -->

# per-environment config (app.Config)

> `app.Config` declares your app's per-environment settings — values that differ between development and production, like an invite base URL or a from-address. Each is `new Setting("Name")` with an optional `Default`; the real per-environment values come from checked-in `.env.development` / `.env.production` files. Read a setting anywhere with the `Config.Name` handle — in a function body and in a component — and it resolves to the value for the environment the app is running in.

<!-- id: config-app-config · area: config · stability: stable · html: https://osysharp.com/reference/config/app-config/ -->

## Summary        {#summary}
`app.Config` declares the **per-environment settings** your application uses — non-secret values that differ between
development and production, such as an invite base URL (`http://localhost:8099` locally, `https://app.example.com` in
production) or a from-address. It is the **non-secret twin of [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/)**: you declare a setting's **name** in
source and supply its per-environment **values** out of band, but — unlike a secret — a setting's value is public and is
**readable everywhere** through the `Config.Name` handle, in a function body and in a component alike.

```osy syntax
app.Config = [
  new Setting("InviteBaseUrl") { Default = "http://localhost:8099" },
  new Setting("FromAddress")   { Default = "noreply@localhost" },
];

// read it anywhere by handle — it resolves to the running environment's value:
string invite = Config.InviteBaseUrl + "/accept?token=" + token;
```

## Signature      {#signature}
```osy syntax
app.Config = [                                       // one entry per named setting
  new Setting("Name") { Default = "…" },             // Default: the value used when no .env supplies one
  new Setting("Name"),                               // no Default: the value is REQUIRED from a .env file
];

Config.Name                                          // the setting's value for the running environment (a string)
```

`app.Config` is a list — an app may declare as many named settings as it needs.

## Description    {#description}
A setting has a **name** (a string literal, the `Config.Name` read key) and an optional **`Default`**. The name and the
`Default` are the only things that live in source. The actual per-environment values come from two checked-in files that
sit next to your `app.osy`:

- **`.env.development`** and **`.env.production`** — plain `KEY=VALUE` files (`#` comments and blank lines ignored).
  A key names a setting; its value is that setting's value for that environment.

These `.env` files are **compile inputs**, not runtime reads: when you compile, both value sets are baked into the app,
so one compiled app carries its development *and* its production values and needs no per-environment recompile. For each
setting, the value in each environment is the `.env` override if present, otherwise the setting's `Default`.

At runtime the app **selects** the set matching the environment it is running in — a local dev server resolves the
development values, a production deployment resolves the production ones — so `Config.InviteBaseUrl` reads
`http://localhost:8099` locally and `https://app.example.com` in production, from the same build.

**Reading a setting.** `Config.Name` is a plain string value. Use it in a function body (building an email link,
choosing a from-address) and in a component (a link, a label). Reading a setting that is not declared in `app.Config` is
a compile error — a typo never silently reads as empty.

**Required vs. defaulted.** A setting with a `Default` always has a value. A setting **without** a `Default` is
*required*: if either `.env.development` or `.env.production` does not supply it, the compile fails — you cannot ship a
build that is missing a value in an environment it targets. An `.env` key that matches no declared setting is likewise a
compile error, so a stale or mistyped key is caught rather than silently ignored.

**Not for secrets.** `app.Config` values are stored and served in the clear — they are meant to be public (a URL, an
address), and a component can read them. An API key, token, or password belongs in [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/), which keeps its
value out of source and never sends it to a browser. A config value that looks like a secret (a long random token, an
`sk-…` key) is flagged with a warning nudging you toward `app.Secrets`.

## Examples       {#examples}
Declare two settings with development defaults, and read one in a function that builds an invite link:

```osy title="basic" test app=config-app-config-example
app.Config = [
  new Setting("InviteBaseUrl") { Default = "http://localhost:8099" },
  new Setting("FromAddress")   { Default = "noreply@localhost" },
];

string InviteLink(string token) {
  return Config.InviteBaseUrl + "/accept?token=" + token;
}
```

To give production a different value, add a checked-in `.env.production` next to your `app.osy`:

```text title=".env.production"
InviteBaseUrl=https://app.example.com
FromAddress=hello@example.com
```

Now `Config.InviteBaseUrl` reads `http://localhost:8099` when the app runs locally and `https://app.example.com` when it
runs in production — from one compiled build, with no code change.

## See also       {#see-also}
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — the SECRET twin: a value kept out of source and never served to the client
- [embedding provider (app.Embedding)](https://osysharp.com/reference/config/embedding/) — another `app.X` config block (the embedding model)
- [default LLM model (app.DefaultModel)](https://osysharp.com/reference/agent/default-model/) — `app.DefaultModel`, the app's default LLM config


---

<!-- https://osysharp.com/reference/config/workflow/ -->

# workflow run retention (app.Workflow)

> `app.Workflow` declares how long the app keeps FINISHED workflow runs. Without it a completed run is kept for ever, along with everything it owns — its work items, its timers and its transition trail. With it, a run whose terminal state is older than the window is reaped, and its children go with it. A run still in progress is never reaped, whatever its age.

<!-- id: config-workflow · area: config · stability: stable · html: https://osysharp.com/reference/config/workflow/ -->

## Summary        {#summary}
`app.Workflow` declares how long the app keeps **finished** workflow runs. A run that reaches a terminal state still
occupies a row, and so does everything it owns — its work items, its timers, its stored step results and its
transition trail. Nothing removes them unless you say so, so an app accumulates every run it has ever executed.

Declaring a `Retention` window changes that: a run whose completion is older than the window is reaped on a
background sweep, and its children are reaped with it. Two things are guaranteed and neither is configurable — **a
run that has not finished is never reaped, however old it is**, and **with no window declared nothing is ever
reaped**. Deleting an app's data on a default nobody chose is not a decision the platform makes for you.

```osy syntax
app.Workflow = new WorkflowConfig { Retention = TimeSpan.FromDays(90) };
```

## Signature      {#signature}
```osy
app.Workflow = new WorkflowConfig {
  Retention = TimeSpan.FromDays(90),   // (optional) how long a FINISHED run is kept after it completes
};
```

`Retention` takes the same constant `TimeSpan.From…` factory that [`app.Audit`](https://osysharp.com/reference/config/audit/) retention takes —
`FromDays`, `FromHours`, `FromMinutes`, `FromSeconds`. It must be a constant and it must be positive; a computed
value or a bare number is a compile error rather than a window nobody can predict.

## Description    {#description}

### What "finished" means, and why age alone is never enough   {#finished}
The window is measured from the moment a run **completed** — not from when it started, and not from when it was last
touched. A run that succeeded, failed or was cancelled is finished; a run that is still executing or waiting on an
event is work in progress and is out of scope entirely.

This distinction is the whole safety property. A long-running workflow — an annual review, a multi-year warranty, a
contract that waits on a renewal that has not come — can easily be older than any window you would pick, and it must
survive. Age is only ever consulted for a run that has already reached a terminal state, so a live run cannot be
selected at all.

### What goes with the run   {#owned-rows}
A finished run owns the rows that describe how it ran: the work items it opened, the timers governing them, the
results of the steps it executed, and its transition trail. These are **structurally owned** — they describe that one
run and mean nothing without it — so reaping a run reaps them together. There is no state in which the run is gone
and its work items remain pointing at nothing, which is worse than either keeping or removing the lot.

A run that another run still names as its parent is left alone until that child has itself been reaped. Lineage stays
intact; the sweep simply picks it up on a later pass.

### ⚠ It is a CEILING on your workflow audit history   {#audit-ceiling}
`app.Audit.WorkflowAuditRecord.Retention` (see [audit read access (app.Audit)](https://osysharp.com/reference/config/audit/)) sets how long the **transition trail** is kept. The
trail belongs to its run, so **the run window takes it with it**: once the run is reaped its trail is gone, whatever
the audit window says.

That means an audit window **longer** than the run window cannot be honoured, and the compiler refuses the pair
rather than quietly obeying the shorter one:

```text
`app.Audit.WorkflowAuditRecord.Retention` is TimeSpan.FromDays(365), but `app.Workflow.Retention` is
TimeSpan.FromDays(30) — and the run window takes the audit trail with it. A workflow audit event belongs to its run
(the run is reaped, its events go too), so the longer window cannot be honoured. Raise `app.Workflow.Retention` to
at least TimeSpan.FromDays(365), or lower this one.
```

An equal window is fine — the run and its trail expire together. A **shorter** audit window is also fine, and is how
you keep runs longer than you keep their transition detail. If you need the trail to outlive the run, do not set a
run window at all.

### Removing the block stops the reaping   {#removing}
`app.Workflow` is reconciled on every compile, so deleting it from source removes the policy — it does not leave the
last window it ever had quietly in force. For a setting whose job is deleting data, the difference between "we
stopped reaping" and "we go on reaping on a rule nobody can see any more" is the whole point.

### When to reach for it   {#when}
Reach for a window when runs are numerous and short-lived and their history has no ongoing value — a per-request
approval, a per-order fulfilment, a notification flow. Leave it undeclared when the run IS the record: anything you
would expect to look up years later, or anything whose trail answers a compliance question. Keeping data costs
storage; a window you set too tight costs you the answer to a question you have not been asked yet.

## Examples       {#examples}

Ninety days of finished runs, kept bounded without touching anything still in flight:

```osy title="basic" test app=config-workflow-retention
entity Order {
  [Required, MaxLength(200)] string Reference;
  OrderStage Stage = OrderStage.Placed;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

enum OrderStage { Placed, Shipped }

workflow Fulfilment {
  Tracks  = Order.Stage;
  Initial = Placed;
  event Ship();
  state Placed {
    subscribe Ship();
    on Ship { goto Shipped; }
  }
  terminal success Shipped { }
}

app.Workflow = new WorkflowConfig { Retention = TimeSpan.FromDays(90) };
```

Keeping the runs for a year but their transition detail for only a month — the audit window is *shorter*, which the
run window permits:

```osy title="trail-expires-first" test app=config-workflow-trail
[Principal] entity Staff {
  [Required, MaxLength(200)] string Name;
  bool IsAuditor;
}

app.Workflow = new WorkflowConfig { Retention = TimeSpan.FromDays(365) };

app.Audit = new AuditConfig {
  WorkflowAuditRecord = new AuditSurface { Read = user => user.IsAuditor, Retention = TimeSpan.FromDays(30) }
};
```

## See also       {#see-also}
- [audit read access (app.Audit)](https://osysharp.com/reference/config/audit/) — `app.Audit.WorkflowAuditRecord`, the transition trail this window is a ceiling on
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — the run this window measures, and what reaching a terminal state means
- [use](https://osysharp.com/reference/types/use/) — bringing the workflow capability into the app


---

<!-- https://osysharp.com/reference/counter/declaration/ -->

# Counter

> The PLATFORM assigns this number at create and you never set it — invoice codes, ticket numbers. NOT a manual order a person rearranges (see [[ui-reordering]]). A gap-free sequence; Format turns it into a code string, and scope restarts it per parent row.

<!-- id: counter-declaration · area: counter · stability: stable · html: https://osysharp.com/reference/counter/declaration/ -->

## Summary        {#summary}
A `Counter` assigns a **sequential number when a row is created** — the order number, the invoice code, the ticket
number. The platform allocates it, so two requests creating rows at the same instant cannot receive the same value.

This is not the row's identity: every entity already has an `Id`. A counter is the number a *human* uses — the one
they read down the phone.

## Signature      {#signature}
```osy syntax
Counter <Name> = new() {
  Start     = <n>,                    // the seed; the first value assigned is Start + Increment
  Increment = <n>,                    // default 1
  Format    = $"…{Value:D4}…",        // optional — makes the value a formatted CODE string
};

entity <E> {
  [Counter(<Name>)] int <Member>;                     // the raw number
  [Counter(<Name>)] string <Member>;                  // the formatted code (needs a Format)
  [Counter(<Name>, scope = <Ref>)] int <Member>;      // restarts per parent row
  [Counter(<Name>, scope = <Ref>)] string <Member>;   // a formatted code that restarts per parent
}
```

## Description    {#description}

### A plain sequence — 1, 2, 3   {#plain}
Declare the counter, then tag the member with the **`[Counter]`** attribute — `[Counter(Name)]` naming the counter to
draw from. You never assign it — creating the row does, and the compiler enforces it: writing a counter member
anywhere (an initializer, an assignment, a bulk `Update` body) is a compile error. Reading and filtering on it are
ordinary:

```osy title="an auto-assigned order number" test app=counter-declaration
Counter OrderNumber = new() { Start = 1000 };

entity Order {
  [Counter(OrderNumber)] int Number;
  [Required] string CustomerName;
}

void NewOrder(string customer) {
  var o = new Order { CustomerName = customer };
  // Number is assigned here — 1001 for the first order, then 1002, 1003 …
}
```

Note the first value is **`Start + Increment`**, not `Start`: `Start = 1000` gives you 1001 first. Read `Start` as
"the number already used", not "the number I want first".

### A formatted code — `INV-0001`   {#format}
`Format` is an interpolated string with a `Value` placeholder, and it turns the sequence into the code your business
actually uses. Tag a `string` member to get it:

```osy title="a formatted invoice code" test app=counter-declaration
Counter InvoiceNumber = new() { Start = 0, Format = $"INV-{Value:D4}" };

entity Invoice {
  [Counter(InvoiceNumber)] string Code;   // "INV-0001", "INV-0002", …
  decimal Total;
}
```

`:D4` is the C# format specifier for "at least 4 digits, zero-padded". Pad generously — a code that jumps from
`INV-9999` to `INV-10000` will sort wrongly in every spreadsheet it ever lands in.

#### What can go in the template?   {#holes}
Exactly three, each with an optional `:format`:

| hole | fills with |
|---|---|
| `{Value}` | the number |
| `{DateTime.UtcNow}` · `{DateTime.Now}` | the current instant, UTC — the two spellings fill identically |
| `{DateTime.UtcNow.InZone(<IANA zone>)}` | that instant read as a zone's wall-clock time |

Anything else is a **compile error**. It used to be copied into the value verbatim, so `$"INV-{Vaule:D4}"` printed
`INV-{Vaule:D4}` on every row until somebody noticed in the data.

⚠ **A bare instant hole is UTC**, whichever way it is spelled — see [Current time (DateTime.UtcNow, DurableClock.Now)](https://osysharp.com/reference/function/current-time/). `{DateTime.Now}` once
rendered the *server's local* time here, which is the wrong year for anyone reading the invoice from another zone,
and wrong at a boundary nobody tests. Name the zone when the number should carry a local year:

```osy syntax
Counter InvoiceNumber = new() { Start = 0, Format = $"INV-{DateTime.UtcNow.InZone(Europe/Stockholm):yyyy}-{Value:D4}" };
```

The zone id is **unquoted**, unlike `Zone.Of("Europe/Stockholm")` in an expression — the template already lives inside
a string, so a nested quote would end it. An IANA id has no spaces or brackets, so nothing is ambiguous without them.

### Counting by something other than 1   {#increment}
```osy title="a sequence that steps by ten" test app=counter-declaration
Counter BatchNumber = new() { Start = 0, Increment = 10 };   // 10, 20, 30 …

entity Batch {
  [Counter(BatchNumber)] int Seq;
  [Required] string Name;
}
```

### A sequence that restarts per parent   {#scope}
`scope` restarts the sequence for each distinct value of a reference — so each project's tickets are numbered from 1,
which is what people expect when they say "ticket 3 on the Apollo project":

```osy title="ticket numbers that restart for each project" test app=counter-declaration
Counter TicketSeq = new() { Start = 0 };

entity Project {
  [Required] string Name;
}

entity Ticket {
  [Required] string Title;
  Project Project;
  [Counter(TicketSeq, scope = Project)] int Seq;   // 1, 2, 3 … within EACH project
}
```

Without `scope` the numbers would be global — Apollo's first ticket might be 4,812, because Mercury used the first
4,811. With it, every project starts at 1.

#### Both at once — a formatted code that restarts per parent   {#scope-and-format}
`scope` and `Format` combine, and the combination is the shape most businesses actually want: each customer's
invoices numbered from one, in the customer-facing code:

```osy title="each organization numbers its own reports from SF-0001" test app=counter-declaration
Counter ReportNumber = new() { Start = 0, Format = $"SF-{Value:D4}" };

entity Organization {
  [Required] string Name;
}

entity ExpenseReport {
  [Required] string Title;
  Organization Org;
  [MaxLength(20)] [Counter(ReportNumber, scope = Org)] string Number;   // SF-0001, SF-0002 … per org
}
```

⚠ **A scoped counter has to read its own numbers back**, because it continues from the highest one already issued to
*that* parent — and on a `string` member the column holds the code, not the number. So the format must let the number
be found again, and one that does not is a **compile error** rather than a wrong number on somebody's invoice:

| format | why it is refused |
|---|---|
| `$"SF-{DateTime.UtcNow:yyyy}"` | never prints `{Value}` — there is no number in the code to continue from |
| `$"SF-{Value:D4}{DateTime.UtcNow:yyyy}"` | `{Value}` sits against a date hole: `00012026` says nothing about where the number ends. Put a separator between them |
| `$"{Value}-{Value}"` | two copies of the number give two answers |

None of these applies without `scope` — nothing ever reads an unscoped counter's value back, so every format that
renders is legal there.

### Can I use it as the primary key?   {#not-the-id}
Do not use a counter as the primary key, and do not use the `Id` as an order number. They answer different questions:
the `Id` identifies the row to the system, the counter names it to a person. Both exist because both are needed.

## See also       {#see-also}
- [entity](https://osysharp.com/reference/entity/declaration/) — the `Id` every entity already has
- [constraints](https://osysharp.com/reference/entity/constraints/) — `[Unique]`, for when the value comes from outside rather than a sequence
- [entity members](https://osysharp.com/reference/entity/properties/) — the member types a counter can fill


---

<!-- https://osysharp.com/reference/diagnostics/index/ -->

# Diagnostics (saying something, and silencing something)

> What your app says while it runs, and what the compiler says about it. The first-day mistake is reaching for a print statement — `Log.*` is dual-sided, so one call from a page lands in the browser console AND in `osy logs` under one correlation id spanning client and server.

<!-- id: diagnostics-index · area: diagnostics · stability: stable · html: https://osysharp.com/reference/diagnostics/index/ -->

## Summary        {#summary}
**Two things live here: what your app says at run time, and what the compiler says at build time.**

```osy title="one call, both sides of the wire" test app=diagnostics-index
entity Order {
  [Required, MaxLength(40)] string Code;
  bool Shipped;
}

void ShipOrder(Order order) {
  Log.Information("Order {OrderCode} shipped", order.Code);
  order.Shipped = true;
}
```

## Description    {#description}
**`Log.*` is dual-sided and always available** — no dependency to declare. Called from a page it reaches the
browser console *and* the app's own log, under one correlation id that spans the client call and the server work it
caused, which is what makes `osy logs --tail` the first move when a page misbehaves. Both the template form and an
interpolated string capture searchable fields; the interpolated form is decomposed, not flattened. See
[Log.*](https://osysharp.com/reference/diagnostics/log/).

**A warning you have decided about is silenced where you decided it** — [[SuppressWarning]](https://osysharp.com/reference/diagnostics/suppress-warning/) — not by
turning the rule off globally. A suppression names the rule and sits at the site, so the next reader sees the
decision rather than an absence.

**For what a run actually did, rather than what it said**, the local area has the tools:
[Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/) opens a trace and jumps to the fault, and faults are captured even with tracing off.

## See also   {#see-also}
- [Log.*](https://osysharp.com/reference/diagnostics/log/) — the one logging call, and why it is dual-sided
- [[SuppressWarning]](https://osysharp.com/reference/diagnostics/suppress-warning/) — silencing one rule at one site
- [Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/) — when the log is not enough and you want the run


---

<!-- https://osysharp.com/reference/diagnostics/log/ -->

# Log.*

> Write a line to your app's structured log. Both a plain template (`"Order {OrderCode} shipped", order.Code`) and an interpolated string (`$"Order {order.Code} shipped"`) capture searchable fields — the interpolated form is decomposed, not flattened. Always available; no dependency to declare. Read the lines back with `osy logs`.

<!-- id: diagnostics-log · area: diagnostics · stability: stable · html: https://osysharp.com/reference/diagnostics/log/ -->

## Summary        {#summary}
**`Log.*`** writes a line to your app's structured log — the one `osy logs` reads back.

```osy syntax
Log.Information("Order {OrderCode} shipped with {Qty} items", order.Code, order.Qty);
```

A log line is not just text. The `{OrderCode}` and `{Qty}` slots become **searchable fields** on the stored line, so
you can later ask for *every* line where `OrderCode` was `A-1`, rather than grepping for a substring.

You do **not** declare a dependency to use it. Logging isn't network access or data access, so — unlike the HTTP and
storage surfaces — there is nothing to switch on. `Log.*` is simply always there.

`Log.*` works the same in **server-side code and in UI actions** — same spelling, same levels, same fields. A line
written in the browser goes to the browser console *and* to your app's log, so `osy logs` shows both sides of a click
together.

## Signature      {#signature}
```osy syntax
void Log.Verbose    ([Exception e, ] string message [, values…])
void Log.Debug      ([Exception e, ] string message [, values…])
void Log.Information([Exception e, ] string message [, values…])
void Log.Warning    ([Exception e, ] string message [, values…])
void Log.Error      ([Exception e, ] string message [, values…])
void Log.Fatal      ([Exception e, ] string message [, values…])
```

- **`message`** — a template with `{Field}` slots, an interpolated string, or a plain string.
- **`values`** — one value per `{Field}` in the template, in order. Not used with an interpolated string (its holes
  already *are* the values).
- **`e`** (optional, on any level) — an exception to attach to the line, with its stack trace.

The six levels, quietest to loudest: **Verbose** (tracing detail, normally switched off) · **Debug** (detail useful
while developing) · **Information** (the app's normal running commentary) · **Warning** (something is off, but the
operation continued) · **Error** (an operation failed) · **Fatal** (the app cannot continue).

## Description    {#description}

### Two ways to write a line — both keep the fields   {#forms}
The template form names its fields explicitly:

```osy title="the template form — field names written down" syntax
Log.Information("Order {OrderCode} shipped with {Qty} items", order.Code, order.Qty);
```

The interpolated form reads more naturally, and captures exactly the same fields:

```osy title="the interpolated form — same fields, nothing destroyed" syntax
Log.Information($"Order {order.Code} shipped with {order.Qty} items");
```

Both render the identical message, and both store `OrderCode`/`Qty` (template form) or `OrderCode`/`OrderQty`
(interpolated form) as fields you can search on. **The structure is a bonus you never pay for** — the line reads the
same either way.

This is worth dwelling on, because it is the opposite of what most languages do. Elsewhere, putting an interpolated
string into a logger *destroys* the structure: the values are pasted into one flat string before the logger ever sees
them, which is why linters warn you away from it. Here the natural spelling is also the correct one.

**Field names in the interpolated form** are derived from what's in the hole: `{order.Code}` becomes `OrderCode`,
`{count}` becomes `Count`. A hole that isn't a simple path — `{order.Total * rate}` — has no natural name, so it is
stored positionally (`arg0`).

**Which form should you use?** If a dashboard or an alert depends on a field name, use the **template form** — the
name is written down, so renaming a variable can't silently change it. For everyday logging, the **interpolated form**
is shorter and reads better. Both are first-class.

### Formatting a value   {#formatting}
A format specifier works in either form, exactly as it does elsewhere in the language:

```osy title="a format specifier, in either form" syntax
Log.Information("Charged {Amount:N2} to {Card}", amount, card);
Log.Information($"Charged {amount:N2} to {card}");
```

### A message you computed   {#computed-message}
If the message is a value rather than a template, it still logs — as a plain line, with no fields:

```osy title="a message you computed — a plain line, no fields" syntax
var msg = BuildSummary(order);
Log.Information(msg);
```

That is allowed and sometimes exactly right. But there is nothing for values to bind to, so passing extra values
alongside a computed message is an error. Switch to a template when you want fields back.

### Recording a failure   {#exceptions}
Attach the exception itself rather than pasting its message into the text — the stack trace is stored with the line:

```osy title="attach the exception, do not paste its message" syntax
try {
  ChargeCard(order);
} catch (Exception e) {
  Log.Error(e, "Charge failed for {OrderCode}", order.Code);
}
```

### What you never have to pass   {#ambient-context}
Every line automatically carries the context it was written in: which request it belongs to (its correlation id),
which app, which user, and which function was running. You never pass any of that — it is added for you, and it is
what lets you pull up **every line from one request**, across everything it touched:

```bash
osy logs --corr a1b2c3d4e5f6
```

That holds even when a function pauses and resumes later: both halves belong to the same request.

Lines written from background work — a scheduled task, a workflow step — carry the correlation id of the request that
*started* the work, so a chain that leaves the request and comes back is still one trace.

### One click, one trace   {#correlation}
This is the part worth understanding, because it is what the whole surface is for.

When a user clicks something, the action that runs, the log lines it writes, and every server function it calls all
share **one correlation id**. So a bug that starts in the browser and ends in a server function is *one* query:

```bash
osy logs --corr c-a1b2c3d4e5f6
```

You will see the browser's lines and the server's lines interleaved, in order — not two disconnected halves.

That includes the line you most want: **an action that throws is logged automatically**, at `Error`, with its exception
and under that same id. You do not have to wrap your actions in `try`/`catch` to find out that one failed.

### Reading the lines back   {#reading}
`osy logs` reads your app's log back from the local platform:

```bash
osy logs                            # the recent lines
osy logs --tail                     # follow live, printing new lines as they are written
osy logs --level error,warning      # only these levels (exact — comma-separated means "any of")
osy logs --function-name ShipOrder  # only lines written while this function ran
osy logs --corr a1b2c3d4e5f6        # every line from one request, across everything it touched
osy logs --grep "timed out"         # search the message text
osy logs --side client              # only the lines your app wrote in the BROWSER
osy logs --side server              # only the lines it wrote server-side
```

The same command exists against a deployed app as `osyrin app logs`, with the same filters — except `--tail`, which is
local-only for now.

### A note on browser lines in production   {#browser-lines}
Lines written in the browser are sent to your app's log, so you can see what a real user's browser actually did. To keep
that from becoming noise (and cost), only `Warning` and above are sent from a deployed app by default — locally you get
everything. Set `Log:ClientMinimumLevel` to change it.

## Examples       {#examples}

Log a milestone with searchable fields, and record a failure with its exception:

```osy title="log a milestone, and a failure" test app=log
entity Order {
  [Required] string Code;
  int Qty;
  bool Shipped;
}

void ShipOrder(Order order) {
  Log.Information("Order {OrderCode} shipped with {Qty} items", order.Code, order.Qty);
  order.Shipped = true;
}

void ChargeOrder(Order order) {
  try {
    throw new Exception("card declined");
  } catch (Exception e) {
    // The exception rides along with the line — stack trace and all.
    Log.Error(e, "Charge failed for {OrderCode}", order.Code);
  }
}
```

The interpolated form — same fields, less ceremony:

```osy title="the interpolated form captures fields too" test app=log
void Audit(Order order) {
  var qty = order.Qty;
  Log.Debug($"Recomputing totals for {order.Code} ({qty} items)");
  Log.Warning($"Order {order.Code} has no line items");
}
```

## See also       {#see-also}
- [Http.*](https://osysharp.com/reference/http/facade/) — the outbound-HTTP surface, whose calls appear in these same logs


---

<!-- https://osysharp.com/reference/diagnostics/suppress-warning/ -->

# [SuppressWarning]

> Accept ONE named warning about ONE declaration. Put it on whatever the finding is reported against — a function, an entity, one FIELD of an entity, a component or a component member, a workflow state or one of its members — naming the code the warning itself prints. Everything else that declaration earns still arrives, errors are never affected, and a MUST-tier lint finding is reported however it is annotated.

<!-- id: diagnostics-suppress-warning · area: diagnostics · stability: stable · html: https://osysharp.com/reference/diagnostics/suppress-warning/ -->

## Summary        {#summary}
**`[SuppressWarning("CODE")]`** is how you say *I read that one, and I mean it*.

```osy syntax
[SuppressWarning("cost-unbounded-read")]
live var everyRoom = Room.ToList();
```

A warning tells you something about your program that is usually a mistake. Some programs are correct **and** earn
one anyway. Without a way to say so, you are left choosing between changing correct code and living with a message
you have to re-read and re-dismiss every build — and a warning people learn to skip past has stopped working, for
your case and for the next one.

## Signature      {#signature}

```text
[SuppressWarning("CODE" [, "CODE"…])]
```

- **`CODE`** — the diagnostic code, exactly as the warning prints it — a compiler code (`ENUM_VALUE_REINTERPRETED`)
  or an `osy lint` rule id (`cost-unbounded-read`). String literals only.
- Placed on **whatever the finding is reported against** — see [Where does it go?](#where).
- Several codes in one attribute, or several attributes, both work.

## Description    {#description}

### Where does it go?        {#where}

**On the thing the finding is reported against, at the granularity it names.** Every finding prints a target and a
line; put the attribute on the declaration that line belongs to. All of these carry it:

| the finding names… | write it on |
|---|---|
| an app-wide shape | the `app { }` manifest |
| a function | the `void Foo(…)` declaration |
| an entity, or the entity's `security { }` | the `entity Foo` declaration |
| **one FIELD of an entity** (`User.Email`) | **that field** |
| a component, or its page | the `component Foo()` declaration |
| a component member | that `action` / `live var` / field / lifecycle hook |
| a workflow state (`Flow.Pending`) | that `state` |
| **a member of a state** (`Flow.Pending.Acceptance`) | **that member** — the `subscribe` slot, the `enter` / `exit` block, the `on` handler, or the `Assigned` / `Finished` milestone the finding is reported in |
| a workflow event | the `event` declaration, beside its `[Authorize]` |

A dotted target is the tell: `User.Email` is a **field**, so the attribute goes on the field, not on `User`. Putting
it on the entity would work too — an entity's suppression covers its whole body — but it accepts more than you
looked at, and the next field to earn the same finding is silenced by a decision nobody made about it.

### A MUST is not suppressible        {#must}

`osy lint` sorts findings into MUST / SHOULD / CONSIDER. A **SHOULD or a CONSIDER is advice**, and recording a
decision about advice is exactly what this attribute is for. A **MUST is the ship gate** — `osy lint --strict`
exits non-zero on one — so it stays reported however it is annotated, and the finding tells you so in a sentence
rather than going quiet. That holds at every placement, a field included: widening *where* the attribute may be
written never widens *what* it can silence.

### It covers ONE declaration        {#scope}

The suppression applies to the declaration it sits on, and to nothing else. There is deliberately no file-wide or
app-wide form: you are accepting **one warning about one thing**. A file-level switch would go on silencing the same
warning in code written later, by someone who never made that judgement — which is the moment a suppression stops
being a decision and becomes a blindfold.

### It covers ONE code        {#one-code}

Naming a code silences that code. Every other warning the declaration earns — today's and tomorrow's — still
arrives. That is what makes the attribute safe to leave in place: it does not turn anything off, it answers one
question.

There is no bare `[SuppressWarning]`. It would mean "and whatever else this earns", which is the one thing you are
not in a position to agree to yet, so the compiler asks for the code instead.

### It never touches an error        {#not-errors}

Only warnings can be suppressed. An error says your program does not have a defined meaning; that is not a matter of
opinion and there is nothing to accept.

### Say WHY, in a comment        {#why}

The attribute records that you decided; it cannot record what you knew. A reader six months later needs the reason,
and the reason is usually a sentence:

```osy syntax
// There are eleven rooms and there will never be more — this is a fixed set, not a growing table.
[SuppressWarning("cost-unbounded-read")]
live var everyRoom = Room.ToList();
```

If you cannot write that sentence, the warning is probably right.

## Examples       {#examples}

### Accepting the commit-agency warning        {#example-commit}

A board reads its whole column list on every visit. `cost-unbounded-read` is right that an unbounded read is usually
a paging bug waiting to happen — and here the set is fixed by the domain, so it is not.

```osy title="a read that is unbounded on purpose" test app=diagnostics-suppress-warning
[Principal] entity User { [MaxLength(255)] string Email; }

entity Column {
  [MaxLength(80)] string Title;
  security { allow read, create when IsAuthenticated; }
}

[Page("/board")]
component BoardPage() {
  // A board has four columns and always will — this is a fixed set, not a growing table.
  [SuppressWarning("cost-unbounded-read")]
  live var columns = Column.ToList();

  render {
    Stack(gap: 2) {
      foreach (var c in columns) { Text(c.Title); }
    }
  }
}
```

### Does one suppression cover the rest of the file?        {#example-scope}

A suppression on one member says nothing about another. If a second member earns the same finding, it gets it — and
that is the point: the first decision was about the first member.

```osy syntax
[SuppressWarning("cost-unbounded-read")]
live var everyRoom = Room.ToList();       // accepted

live var everyBooking = Booking.ToList(); // still reported — nobody has said anything about this one
```

### Accepting a finding about one FIELD        {#example-field}

`data-constraint-lets-null-through` asks a real question about a `[Unique]` field that can be absent: two rows with
no value do not collide, so the constraint does not mean "every row has one". Here the field is genuinely optional,
so the answer is "yes, that is what I meant" — and the finding is scoped to the field, so the attribute goes there.

```osy title="a constraint that lets null through, on purpose" test app=diagnostics-suppress-warning-field
[Principal] entity User {
  [Unique, MaxLength(200)] string Email;
  [MaxLength(200)] string PasswordHash;

  // A referral code is optional — most people sign up without one, and two blanks are not a clash.
  [SuppressWarning("data-constraint-lets-null-through")]
  [Unique, MaxLength(40)] string? ReferralCode;

  security {
    allow read when IsAuthenticated;
    // Nothing to do with suppression — a credential on the [Principal] rides `Session.CurrentUser` to the
    // browser without it. See [app.Auth — how the platform authenticates a user of your app](https://osysharp.com/reference/security/password-auth/).
    deny read PasswordHash when IsAuthenticated;
  }
}
```

Note the `?`. A bare `string ReferralCode;` is **required by its spelling**, so it has no absent case and the
finding never fires on it — a suppression there would silence nothing.

### Accepting a finding about a workflow slot, or an `enter` block        {#example-workflow}

An invitation is answered by whoever holds the emailed link. Two findings are right to ask about that: the slot
declares no `Candidates` (`workflow-slot-open-to-everyone`), and the link minted in `enter` steps around the event's
`[Authorize]` (`security-callback-url-widens-an-authorize`). Both are the design here, so both are answered where
they are reported — on the slot, and on the `enter` block. A state member takes the attribute exactly as a
function or a field does, and so does the `state` itself.

```osy title="a slot that is open on purpose, and a link that is meant to widen the rule" test app=diagnostics-suppress-warning-workflow
enum InviteStatus { Pending, Accepted, Expired }

[Principal] entity Person {
  [MaxLength(80)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Invite {
  [MaxLength(200)] string Email;
  InviteStatus Status;
  [MaxLength(400)] string? Link;
  security { allow read, create, update when IsAuthenticated; }
}

workflow Invitation {
  Tracks = Invite.Status; Autostart = true; Initial = Pending;

  [Authorize(u => u.Email == this.Item.Email)]
  event Accept();

  state Pending {
    // Anyone holding the link may accept — the slot is open by design, not by omission.
    [SuppressWarning("workflow-slot-open-to-everyone")]
    subscribe Accept() as Acceptance {
      Finished { Within = TimeSpan.FromDays(14); Unfinished { goto Expired; } }
    }

    // The emailed link is the whole point: the [Authorize] above governs the in-app button, the link the mail.
    [SuppressWarning("security-callback-url-widens-an-authorize")]
    enter {
      this.Item.Link = Acceptance.CallbackUrl();
    }

    on Acceptance { goto Accepted; }
  }

  terminal success Accepted { }
  terminal cancel Expired { }
}
```

Each attribute covers the one member it sits on. A second slot in `Pending` with no `Candidates` would still be
reported — that is a second decision, and nobody has made it yet.

## See also       {#see-also}

- [Log.*](https://osysharp.com/reference/diagnostics/log/) — writing to your app's own log, which is a different question from the compiler's.


---

<!-- https://osysharp.com/reference/entity/equality/ -->

# Comparing entity rows

> `==` on two entity references compares them by ROW IDENTITY — not by object reference, and not field by field. Two references to the same row are equal however you fetched them, so `list.Contains(row)`, `list.IndexOf(row)` and `list.Remove(row)` all find the row you mean. You never have to compare `.Id` yourself.

<!-- id: entity-equality · area: entity · stability: stable · html: https://osysharp.com/reference/entity/equality/ -->

## Summary        {#summary}
Two references to the same row **are** the same row, whichever query each came from:

```osy title="two reads of one row, compared directly" test app=entity-equality
entity Contact {
  [Required] string Name;
  string Email;
}

bool OneRowTwoReads(string name) {
  var a = Contact.Single(c => c.Name == name);
  var b = Contact.Where(c => c.Name == name).First();
  return a == b;                   // true — two reads, one row
}
```

`a` and `b` were fetched by two separate queries and are still equal. **You do not have to write `a.Id == b.Id`.**

## Signature      {#signature}
```osy syntax
a == b        // true when both references are the SAME ROW
a != b        // the negation
a == null     // no row — an unset reference, or a lookup that found nothing
```

## Description    {#description}

### An entity compares by identity, a class by value   {#why}
An entity is a **row in a table**. It has an identity of its own, that identity is what the platform stores, and it
is what `==` asks about. So the rule cuts both ways:

- The **same row read twice** is equal to itself — through two queries, in two orders, on the server or on the
  client. Nothing about how you got hold of it changes the answer.
- **Two different rows are never equal**, however identical their columns. Two rows are two rows.

```osy title="identical columns, still two rows" test app=entity-equality
bool TwinsAreOneRow() {
  var twins = Contact.Where(c => c.Name == "Twin").OrderBy(c => c.Id).ToList();
  return twins[0] == twins[1];     // false — same name, same email, two rows
}
```

A [class](https://osysharp.com/reference/class/equality/) has no identity to compare, so its **contents** are the answer instead. That is the only
difference between the two rules, and it follows from what each thing is.

### Contains, IndexOf and Remove all find the row   {#collections}
Every list operation that takes an element compares with the same `==`, so over a list of rows each of them is asking
*"is this the same row?"* — and each of them gets it right, **including when the list came from one query and the row
from another**:

```osy title="asking a list about a row you already hold" test app=entity-equality
string Positions(Contact one) {
  var ordered = Contact.OrderBy(c => c.Name).ToList();
  return "at=" + ordered.IndexOf(one)              // its position, or -1
       + " has=" + ordered.Contains(one)           // is it in this list at all
       + " gone=" + ordered.Remove(one)            // take that row out of the list
       + " left=" + ordered.Count;
}
```

`one` was never read by the query that built `ordered`, and all three still identify it. The same holds on the client:
a row a screen is holding — off a [`live var`](https://osysharp.com/reference/ui/reactivity/), or handed to an action by a `foreach` — is the same
row the list holds, and these three operations say so.

### FindIndex over .Id is not wrong, only longer   {#by-id}
This is the same question, asked the long way round:

```osy title="the two spellings, and they agree" test app=entity-equality
bool BothWaysAgree(Contact one) {
  var ordered = Contact.OrderBy(c => c.Name).ToList();
  return ordered.FindIndex(c => c.Id == one.Id) == ordered.IndexOf(one);   // always true
}
```

If you have written the `FindIndex` form, **nothing is broken** — it returns the same index, and it is a perfectly
readable thing to have written. It is simply not needed: `IndexOf` already compares by identity, so the lambda is
restating the rule the language applies anyway.

Reach for [`FindIndex`](https://osysharp.com/reference/query/in-memory-linq/) when you genuinely cannot hold the element — you have an id off a URL,
a name typed by a user, or any other *description* of the row rather than the row. That is what it is for.

## See also       {#see-also}
- [entity](https://osysharp.com/reference/entity/declaration/) — what a row is, and how you read one back
- [Comparing classes](https://osysharp.com/reference/class/equality/) — why a `class` compares by its fields instead
- [LINQ over a local list](https://osysharp.com/reference/query/in-memory-linq/) — `IndexOf`, `FindIndex` and the rest of the list verbs
- [Child collections (navigating a relation)](https://osysharp.com/reference/query/collections/) — a parent's child collection, which holds rows the same way


---

<!-- https://osysharp.com/reference/entity/concurrency-check/ -->

# ConcurrencyCheck

> Refuses an update whose value was changed by somebody else since this writer read it. Field-scoped — two people editing different fields of one record are not in conflict — and raised as a ConflictException the app can catch.

<!-- id: entity-concurrency-check · area: entity · stability: stable · html: https://osysharp.com/reference/entity/concurrency-check/ -->

## Summary        {#summary}
`[ConcurrencyCheck]` refuses an update whose value was changed by somebody else **since this writer read it**. It is
what stops the last save winning silently — the failure mode where two people open the same record, both save, and
the first one's work disappears with nothing said to either of them.

It is **field-scoped**: only the fields a write actually sets are checked, so two people editing different fields of
the same record both succeed.

## Signature      {#signature}
```osy syntax
[ConcurrencyCheck]              // on the ENTITY — guards every property
entity Case { … }

entity Case {
  [ConcurrencyCheck] decimal? Amount;    // …or on ONE property
}
```

## Description    {#description}

### What it does
A guarded update only lands if the row still holds the value this writer read. If it does not, the write is refused
and nothing is written — the other person's value stands, and yours is still in front of you to re-apply.

The refusal is a [ConflictException](https://osysharp.com/reference/function/throw/), so you catch it exactly as you catch any other conflict —
and then say what you want done with the write that failed:

```osy syntax
try { c.Amount = amount; UnitOfWork.Commit(); }
catch (ConflictException e) { message = e.Message; UnitOfWork.Discard(); }
```

### Why didn't my catch stick? {#abandon-or-repair}
Catching the refusal does not un-stage the write that caused it. The row is still holding your value, and the
platform commits once more when your function ends — so **the same refusal is raised a second time**, from a place
no `try` of yours can reach.

That is not an accident: it is what lets a catch **repair** the value and have the repair committed for you. So the
catch has to say which of the two you meant, and there are only two:

| you want | write this in the catch |
|---|---|
| **abandon** the write — show the message, keep the other person's value | `UnitOfWork.Discard();` |
| **retry** it — resolve the conflict and save your value after all | set the field again; it commits when the function ends |

Leave out both and the run ends with the refusal you already handled. The second one says so, and names these
two. It is the same rule a refused CONSTRAINT follows, worked through in full at
[[function-throw#the-refused-row]].

### Field-scoped, and why that matters
Only the columns a write SETS are checked. If Anna changes `Amount` and Bo adds a note to the same record, neither is
refused — they were never in conflict.

The alternative, refusing on any change to the row, is simpler and worse: it refuses writes that are not conflicts,
and a conflict message that fires when nothing is wrong is one people learn to click past. That is the failure this
scoping exists to avoid.

### It is one statement, so there is no window
The check rides the UPDATE's own `WHERE`:

```sql
SET amount = @amount WHERE id = @id AND amount IS NOT DISTINCT FROM @was
```

Deciding and writing are the same statement, so nothing can change between them. The two designs that look
equivalent both have a window: re-reading the row and comparing leaves a gap before the write, and consulting the
audit trail is worse still — the trail is written *after* the commit, so a competing writer's entry may not be there
yet.

### What "since it was read" means across a page load
The value compared against is what **this writer read**, not what the row holds now. That distinction is the whole
feature: a person holding a page across a round trip is the only writer who can be stale, and their belief is pinned
when they read it, so it survives being resumed later.

### The message
The refusal names the fields it refused, and **names who changed the row and when** when there is somebody to name —
that comes from the entity-change audit trail. An anonymous write records no user, so the sentence falls back to
"somebody else". If your app turns the trail off, the refusal still works and the sentence loses the name; a lint
rule (`security-concurrency-check-without-its-trail`) says so at compile time.

### What it does not do
It does not tell you somebody else has the record open — that is presence, a different mechanism. It does not lock
anything: nobody is blocked from editing, and a conflict is only possible when two writes genuinely overlap on one
field.

## Examples       {#examples}

```osy title="guard the whole record" test app=concurrency-check
[ConcurrencyCheck]
entity Case {
  [Required, MaxLength(40)] string Reference;
  decimal? Amount;
  [MaxLength(200)] string? Owner;

  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}
```

```osy title="catch the refusal and abandon the failed write" test app=concurrency-check
string Save(Guid id, decimal amount) {
  var c = Case.Single(x => x.Id == id);
  try {
    c.Amount = amount;
    UnitOfWork.Commit();
    return "saved";
  } catch (ConflictException e) {
    UnitOfWork.Discard();          // …or set c.Amount again, to save yours after all
    return e.Message;
  }
}
```

## How do I test it? {#testing}

A conflict needs **two writers**, and a test body is one — so the thing to stage is not two readers, it is a
**settle in between**. Everything one function call stages is a single unit of work however many times it writes,
so the other person's change has to reach the database before yours is attempted:

1. read the row into a variable — this is what *you* believe it says;
2. make the other person's change and `UnitOfWork.Commit()` it;
3. write through the variable from step 1 and commit.

Step 2's commit is the whole trick. Without it there is only one writer and nothing is refused.

⚠ **Two things that look like they matter and do not.** Reading the row twice does not help — both reads give you
the same tracked row, so it is still one writer. Neither does `runas`, in either spelling: a conflict is about
*when* a write settled, never about *who* settled it, and staging one under two principals stages the same single
writer twice.

```osy title="the model under test" test app=concurrency-conflict
[ConcurrencyCheck]
entity Ledger {
  [Required, MaxLength(40)] string Code;
  decimal? Amount;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

/// The OTHER person's save.
void MoveAmount(Guid id, decimal amount) {
  var l = Ledger.Single(x => x.Id == id);
  l.Amount = amount;
}
```

```osy title="staging the two writers" run app=concurrency-conflict
[TestFixture]
void Seed() {
  var l = new Ledger { Code = "L-1", Amount = 100 };
}

[Test(Seed)]
void A_stale_write_is_refused_and_the_other_persons_value_stands() {
  var mine = Ledger.Single(x => x.Code == "L-1");   // 1 — what I believe it says
  MoveAmount(mine.Id, 250);
  UnitOfWork.Commit();                               // 2 — …and somebody else's save lands

  string refused = "";
  try { mine.Amount = 999; UnitOfWork.Commit(); }    // 3 — my save, against what I read
  catch (ConflictException e) { refused = e.Message; UnitOfWork.Discard(); }

  Assert.Contains("changed", refused);
  Assert.Equal(250, Ledger.Single(x => x.Code == "L-1").Amount);
}

// …and the same test WITHOUT step 2's commit refuses nothing, which is what makes that line the mechanism
// rather than a detail of this example.
[Test(Seed)]
void With_no_settle_between_them_there_is_only_one_writer() {
  var mine = Ledger.Single(x => x.Code == "L-1");
  MoveAmount(mine.Id, 250);
  string refused = "";
  try { mine.Amount = 999; UnitOfWork.Commit(); }
  catch (ConflictException e) { refused = e.Message; UnitOfWork.Discard(); }
  Assert.Equal("", refused);
}
```

`Assert.Throws<ConflictException>(() => …)` works too, and it does step 2 for you — it settles whatever is already
staged before running its body. That is convenient and it is also why a test can pass under `Assert.Throws` and
prove nothing about the same code written with `try`/`catch`: the assertion supplied the missing commit.

## See also       {#see-also}
- [constraints](https://osysharp.com/reference/entity/constraints/) — the other per-member rules, checked when the row is written
- [throw](https://osysharp.com/reference/function/throw/) — `ConflictException` and the closed set of fault types
- [try / catch / finally](https://osysharp.com/reference/function/try-catch/) — catching it
- [entity](https://osysharp.com/reference/entity/declaration/) — where the attribute goes


---

<!-- https://osysharp.com/reference/entity/index/ -->

# The data model (entities and their rules)

> An entity is a table — and the rules that are true about it. Not just its columns: what must hold for a row to exist, and who may see or change it, live ON the entity, next to the data, and are enforced for every path that ever touches it. This is where an app starts, and getting it right is most of getting the app right.

<!-- id: entity-index · area: entity · stability: stable · html: https://osysharp.com/reference/entity/index/ -->

## Summary        {#summary}
An **entity** is a table of rows your app stores. But the declaration is not a list of columns — it is everything that
is **true** about those rows:

```osy title="a table, and what is true about it" test app=entity-index
entity Order {
  [Required, Unique, MaxLength(20)] string Code;   // the shape …
  [Required] decimal Total;
  bool Cancelled;

  invariant Total >= 0;                            // … what must hold for a row to exist …

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }         // … and who may touch it
}
```

Three different questions, answered in one place: **what shape is a row** ([entity members](https://osysharp.com/reference/entity/properties/)), **what makes a row
valid** ([constraints](https://osysharp.com/reference/entity/constraints/) · [invariant](https://osysharp.com/reference/entity/invariants/)), and **who may see or change it** ([The security model](https://osysharp.com/reference/security/index/)).

They are enforced for **every** path that ever touches the table — this function, the next one, the UI, a workflow, an
import you write in six months. There is nowhere to write a row that goes around them, so there is nowhere to forget.

## Description    {#description}

### What the platform gives every row   {#free-columns}
You never declare an identifier, and you never declare an audit trail:

| Member | What it is |
|---|---|
| `Id` | the row's identity, a `Guid`, assigned for you |
| `CreatedAt` · `ModifiedAt` | when the row was written, and last changed |
| `CreatedBy` · `ModifiedBy` | **who** — the signed-in principal, not a value your code passes |

`CreatedBy` is worth pausing on, because it is not just an audit column: it is stamped from the **security principal**,
so a rule may authorise on it. That is how ownership works with **no owner field and nothing in your code that assigns
one**:

```osy title="ownership, declared once, with no owner field" test app=entity-index-owner
[Role] enum AppRole { Authenticator, Member }

[Principal]
entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read where Id == user.Id; }
}

entity Note {
  [Required, MaxLength(200)] string Title;

  security {
    allow create when IsAuthenticated;
    allow read, update, delete where CreatedBy == user.Id;   // ← ownership, in one line
  }
}
```

Every reader of `Note` now sees only their own — including `Note.Count()`, which honestly means *"how many are mine"*.

### What an unset member reads back   {#optional}
**The declaration decides**, and it reads exactly as C# does:

```osy title="what the type spelling decides about being unset" syntax
decimal Amount;            // has an honest zero → unset reads 0.
decimal? Amount;           // `?` → may be unset. Unset reads null.
string Title;              // NO honest zero → REQUIRED. You must supply it.
```

Whether a member is required is carried by its **type spelling** — full model in
[Optional and required members](https://osysharp.com/reference/types/optional-and-required/); the shapes are:

| Type | Bare (no `?`, no default) |
|---|---|
| `int` `long` `double` `decimal` `bool` `TimeSpan` — an **honest zero** | reads its zero (`0`, `false`, `0m`); never null |
| `string` `DateTime` `DateOnly` `TimeOnly` `Guid` `Json` `RichText` `Markdown` `Vector` `byte[]`, any **enum** — **no** honest zero | **REQUIRED** — you must supply it |
| an entity reference (`Customer Owner;`) | **optional** — reads `null`; `[Required]` demands it |
| any `T?` (`int?` `string?` `DateTime?` …) | optional — reads `null` |

The reason those types have no zero is worth a sentence: the year `0001` is not a date anybody meant (it sorts first
and matches `Due < today`), all-zero bits is not a `Guid` anyone assigned, and `default(string)` is `null`, not `""` —
an empty string is a value someone *typed*, not the absence of one. Rather than invent a value it would then have to
answer questions about, the platform **requires** you to supply one. Say `?` (`DateTime? Due;`) when "maybe unset" is
genuinely what you mean, or give a default (`string Tag = "";`) when the empty value really is the right starting point.

**A required member is checked when the value becomes real** — for an entity, at **commit**: `new Ticket {}` compiles
(an empty draft is a normal state — a create-form binds inputs to fill it), and the row is refused at save if a
required member is still unset, naming it. `[Required]` on a *reference* opts that one exception into the same rule.

It is also the **only** constraint that rejects an unset value. `[MaxLength]`, `[Unique]`, `[Pattern]` and the rest all
let one through, because "no value" is not a violation of "at most 20 characters" ([constraints](https://osysharp.com/reference/entity/constraints/)).

**The consequence to know about.** An [invariant](https://osysharp.com/reference/entity/invariants/) reads a member, and a member that *can* be unset
reads back `null` — so an invariant over an optional member is asking a question about a value that may not be there:

```osy title="an invariant over an optional member quietly makes it mandatory" syntax
decimal? Total;           // may be unset …
invariant Total >= 0;     // … and the invariant reads it — so a row with no Total is REFUSED
```

That has quietly made `Total` mandatory. If a member takes part in an invariant, either give it a type that always has
a value (`decimal Total;` — the invariant then holds trivially on `0`), mark it `[Required]` (say what you mean), or
guard the invariant with `when` so it only applies to the rows that have the value.

### Where do I write a rule — on the member, or on the row?   {#rules}
Two kinds, and the distinction is not academic — it decides where you write the rule:

- A **constraint** speaks about **one member**: `[Required]`, `[Unique]`, `[MaxLength(n)]`, `[Min]`/`[Max]`,
  `[Pattern]`. ([constraints](https://osysharp.com/reference/entity/constraints/))
- An **invariant** speaks about **the whole row** — a relationship *between* members, optionally guarded so it applies
  only to the rows it should. ([invariant](https://osysharp.com/reference/entity/invariants/))

```osy title="one member, versus the whole row" test app=entity-index
entity Product {
  [Required, MaxLength(80)] string Name;      // a constraint: about Name alone
  bool IsPerishable;
  int ShelfDays;

  invariant ShelfDays <= 30 when IsPerishable  // an invariant: about the ROW — two members, conditionally
    message "Perishable items can have at most 30 shelf days.";

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}
```

Both are enforced **when the row is written**, by the database — not by the function that happens to be writing it. So
a violation arrives as an ordinary `ValidationException` that a caller can catch ([try / catch / finally](https://osysharp.com/reference/function/try-catch/)), and the
faulted write leaves nothing behind:

```osy title="the rule holds, and the bad row does not survive" run app=entity-index
[Test]
void A_row_that_breaks_its_invariant_is_never_written() {
  Assert.Throws<ValidationException>(() => new Product { Name = "Milk", IsPerishable = true, ShelfDays = 90 });

  Assert.Empty(Product.ToList());
}

[Test]
void A_member_left_unset_reads_back_its_zero() {
  var product = new Product { Name = "Salt" };      // IsPerishable and ShelfDays never set

  var stored = Product.Single(p => p.Name == "Salt");

  Assert.Equal(0, stored.ShelfDays);        // int → 0, exactly as a C# field
  Assert.False(stored.IsPerishable);        // bool → false
}
```

The `?` is how you get the other behaviour — and it is the difference between "the shelf life is zero days" and
"nobody has recorded a shelf life". A type with no meaningful zero is optional whether you write the `?` or not:

```osy title="`?` opts out of the zero; a date has no zero to opt out of" run app=entity-index
entity Delivery {
  [Required] string Reference;
  int Attempts;              // has a zero → unset is 0
  int? WeightGrams;          // opted out    → unset is null
  DateTime? ArrivedAt;       // no zero      → optional (`?`), so unset reads null (a bare DateTime would be required)

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

[Test]
void The_question_mark_is_what_distinguishes_no_value_from_zero() {
  var delivery = new Delivery { Reference = "D-1" };   // nothing else set

  var stored = Delivery.Single(d => d.Reference == "D-1");

  Assert.Equal(0, stored.Attempts);      // zero attempts — a real, countable fact
  Assert.Null(stored.WeightGrams);       // nobody weighed it — a different fact
  Assert.Null(stored.ArrivedAt);         // and a date never has a zero to fall back to
}
```

### Relations: the child points up, the parent reads down   {#relations}
A child holds a reference to its parent — that member **is** the foreign key. The parent reads its children back
through a **collection** member:

```osy title="both directions, declared once" test app=entity-index-rel
entity Invoice {
  [Required, Unique, MaxLength(20)] string Number;

  [ForeignKey(Invoice)] Line[] Lines;          // the parent reads DOWN
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

entity Line {
  [Required] Invoice Invoice;                  // the child points UP — this is the FK
  [Required, MaxLength(80)] string Description;
  decimal Amount;

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}
```

**Read children through the collection — never with a query filtered by the foreign key.** `invoice.Lines` is the
navigation; `Line.Where(l => l.Invoice == invoice)` is you re-implementing it by hand, and worse:

```osy title="navigate the graph; do not re-query it" run app=entity-index-rel
[Test]
void A_parent_reads_its_children_through_its_collection() {
  var invoice = new Invoice { Number = "INV-1" };
  var a = new Line { Invoice = invoice, Description = "Design", Amount = 100m };
  var b = new Line { Invoice = invoice, Description = "Build", Amount = 250m };

  var found = Invoice.Single(i => i.Number == "INV-1");

  Assert.Equal(2, found.Lines.Count);
  Assert.Equal(350m, found.Lines[0].Amount + found.Lines[1].Amount);
}
```

**A collection has no order.** It is a set of rows, exactly as a table is — `invoice.Lines[0]` is *a* line, not the
first one you added, and it may differ between runs. When the order matters, ask for it: query the children with an
[`OrderBy`](https://osysharp.com/reference/query/ordering/), and you get the order you asked for.

`[Required]` on the reference is how you say a child **cannot exist without its parent** — an orphan then becomes a
violation rather than a stray row. See [relations](https://osysharp.com/reference/entity/relations/), and [Include (pre-loading relations)](https://osysharp.com/reference/query/include/) for pre-loading a graph you are
about to walk.

### An entity is stored; a `class` is not   {#class}
Declare a **`class`** when the value only ever lives in memory — a computed result, a payload you assemble, a shape you
return. It has no table, no `Id`, no audit columns, and no `security { }`, because there is nothing stored to secure.

The test is simply: *does a row of this outlive the request?* If yes, it is an entity. See [class methods](https://osysharp.com/reference/class/methods/).

### Do I write migrations when the model changes?   {#evolution}
You do not write migrations. `osy compile` reconciles the database with your source, and the two directions are
deliberately not symmetric:

- **Adding is applied.** A new entity, a new member, a new index — the schema moves, and it moves quietly.
- **Removing is *proposed*, not executed.** Delete a member from your source and the column is **not** dropped. The
  compile reports it as a proposed drop and **keeps the data**; dropping it takes an explicit acknowledgement, and in
  **production** an unacknowledged drop is a hard error rather than something that happens while you are not looking.

That asymmetry is the platform refusing to destroy data on the strength of a deletion you may not have meant. It is
not a gap in the tooling — it is the tooling declining to be clever with the one thing it cannot undo.

### Why so much hangs off the entity design   {#the-work}
Most of designing an Osy# app is designing its entities, because so much hangs off them:

- your **queries** are over them, and the security rules are compiled *into* those queries ([Querying data](https://osysharp.com/reference/query/index/));
- your **functions** contain no authorization code, because the entity's rules already do that job
  ([Functions (the unit of work)](https://osysharp.com/reference/function/index/));
- your **UI** names an entity and gets its data.

Time spent getting the model and its rules right is not preparation for the work. It is most of the work.

## See also       {#see-also}
- [entity](https://osysharp.com/reference/entity/declaration/) — declaring one, and what you get for free
- [entity members](https://osysharp.com/reference/entity/properties/) — the members, the types, and optional-by-default
- [relations](https://osysharp.com/reference/entity/relations/) — references, collections, and the FK query you should not write
- [constraints](https://osysharp.com/reference/entity/constraints/) — `[Required]` · `[Unique]` · `[MaxLength]` · `[Min]`/`[Max]` · `[Pattern]`
- [invariant](https://osysharp.com/reference/entity/invariants/) — a rule about the whole row
- [entity Sub : Base](https://osysharp.com/reference/entity/inheritance/) — `entity Sub : Base`, and what a subtype does and does not carry
- [sealed](https://osysharp.com/reference/entity/sealed/) — how a type says no one may derive from it
- [enum](https://osysharp.com/reference/enum/declaration/) — a closed set of values
- [Counter](https://osysharp.com/reference/counter/declaration/) — a sequence the platform hands out
- [The security model](https://osysharp.com/reference/security/index/) — the rules that live on the entity, and why they are in the query
- [Querying data](https://osysharp.com/reference/query/index/) — reading the model back


---

<!-- https://osysharp.com/reference/entity/front-matter/ -->

# [FrontMatter] — a document's header as typed data

> `[FrontMatter]` binds a member to a key of the entity's markdown document's front-matter — the `---` header at the top of the file. Assigning the document fills those members in, so a document's header becomes ordinary, queryable app data rather than an opaque block, and a required member makes its key mandatory.

<!-- id: entity-front-matter · area: entity · stability: preview · html: https://osysharp.com/reference/entity/front-matter/ -->

## Summary        {#summary}
Markdown documents often begin with a `---` header:

```text
---
title: Getting started
summary: How to create your first project.
stability: stable
---

## Overview
…
```

For a document that header is rarely "metadata". It is usually most of the **application's data** — the title is the
navigation, the summary is the search snippet, the tags are the facets. `[FrontMatter]` says so:

```osy syntax
entity DocPage {
  [FrontMatter] string Title;
  [FrontMatter] string? Summary;
  Markdown Body;
}
```

Assign `Body` and those members fill in. They are ordinary members, so everything else about them is ordinary too:
you can query them, index them, put them in a grid, secure them.

## Signature      {#signature}
```osy syntax
[FrontMatter] <type> <Member>;              // key = the member name, camelCased
[FrontMatter("<key>")] <type> <Member>;     // key named exactly
```

## Description    {#description}

### Which key a member takes   {#key}

A bare `[FrontMatter]` derives the key from the member name by **camelCasing** it. Osy# members are PascalCase and
front-matter keys conventionally are not, so this is usually all you need:

| Member | Key |
|---|---|
| `Title` | `title` |
| `FormatVersion` | `formatVersion` |
| `DeprecatedBy` | `deprecatedBy` |

When a key does not follow that convention — or when the member has to be called something else — name it exactly:

```osy title="naming a key that the camelCase rule would miss" syntax
[FrontMatter("format-version")] int? Version;
```

That form is also the answer for a key called `id`. Every entity already has an `Id`, so a member cannot be named
that; call it something else and name the key:

```osy title="the key called id, which no member may be named" syntax
[FrontMatter("id")] string Slug;
```

### Which document it reads   {#document}

The entity's `Markdown` member. You do not name it, because an entity has one document in the ordinary case and
repeating its name on every key would be ceremony.

An entity with **no** `Markdown` member is a compile error — there is nothing to read. An entity with **more than
one** is also a compile error, naming both, because guessing which document was meant would be a coin flip you never
see.

### What binding does, and does not do   {#binding}

**It fills in declared members, on assignment.** Every write of the whole document — `page.Body = text` — re-reads the
header and sets each bound member.

**A key the document omits CLEARS its member.** A member says what the document says *now*, not what an earlier
document said. This is why a key that may be absent should be declared optional:

```osy title="a key the document may omit must be optional" syntax
[FrontMatter] string? Stability;    // the document may not carry it
```

**A required member makes its key mandatory.** This is the useful consequence of the same rule: declare
`[FrontMatter] string Title;` without the `?` and a document that omits `title` is refused at commit, with `Title` named.
The declaration is the schema — front-matter that is checked rather than merely stored.

**A key you did NOT declare is preserved, untouched.** The header is kept whole; declaring three of its ten keys binds
three and leaves the other seven exactly as written. A reader that quietly dropped what it did not understand would
corrupt the document it read.

### Types   {#types}

The header is YAML, so its ordinary shapes work: quoted and unquoted strings, folded blocks (`summary: >`), flow
sequences (`tags: [a, b]`), block sequences, numbers, booleans, and an explicit `null`.

| Declared as | Takes |
|---|---|
| `string` | a scalar (a folded block arrives as one line) |
| `int` / `decimal` / `bool` / `DateTime` / `Guid` | a scalar that parses as one; anything else leaves the member unset |
| a child collection | a list-valued key, one **row** per item — see below |
| `Json` | a list-valued key, keeping its items as a JSON array |

An explicit `null` (`deprecatedBy: null`) is *nothing*, not the text "null".

### A list-valued key   {#list-key}

`tags: [ui, authoring]` is a list, and a list of what you want to *query* is a set of rows. Bind it to a child
collection and each item becomes one:

```osy title="a list-valued key, bound to child rows" syntax
entity DocPage {
  [FrontMatter] [ForeignKey(Page)] DocTag[] Tags;
  Markdown Body;
}

entity DocTag {
  DocPage Page;
  [MaxLength(80)] string Name;
}
```

The rows are ordinary child rows, so the things you wanted them for work with nothing extra:

```osy title="querying those rows like any other child" syntax
DocPage.Where(p => p.Tags.Any(t => t.Name == "ui")).ToList()
```

The element entity must have **exactly one** scalar member — the one each item becomes. None, or more than one, is a
compile error naming them, because there would be nothing to put the item in or no way to tell which member was meant.
The collection must be a real `[ForeignKey]` child collection: a bare `DocTag[]` is an in-memory projection with no
rows behind it, so binding one is a compile error too rather than a write that quietly goes nowhere.

Re-assigning the document **reconciles**: an item that is still there keeps its row (and its id, so anything pointing
at it survives), an item that arrived is created, one that is gone is deleted, and a key the new document omits empties
the collection. A repeated item is one row — a set of tags is a set. A scalar is a one-item list, so `tags: ui` works.

Use `Json` instead when the items are only ever read together and never queried one by one.

A malformed header binds nothing rather than failing the write. Front-matter is content — hand-written, and sometimes
agent-written — and a mistyped header should still store its document.

### Reading back   {#reading-back}

Nothing changes about how you read the document itself: `Body` still returns the markdown, and [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/)
renders it.

### Importing documents   {#importing}

Assigning the member is what runs all of this, so anything that assigns it works — including a data file. A document
can be given inline, or named as a file beside the data file, which is how a real corpus arrives:

```json
{ "entity": "DocPage", "key": "Slug",
  "files": { "Body": "" },
  "rows": [ { "Body": "pages/controls.md" }, { "Body": "pages/slots.md" } ] }
```

Two things about that are worth knowing:

- **A document's file reference brings its TEXT**, not a stored path. Unlike an image column — which stores the bytes
  and records where they were put — a document has no path; it becomes sections. So it needs no `public/` prefix.
- **The rows carry no `Slug`.** A page's identity is its own `id:` key, and the import reads the key from the
  document's header when the row does not give it. That is what makes re-importing a corpus converge instead of
  doubling it, and it keeps one fact in one place — the document is the thing that is true.

If neither the row nor the header carries the key, the import says so and names both places it looked.

## Examples       {#examples}

A page whose header is its data — the title drives the nav, the summary the listing, and both are queryable:

```osy title="a docs page" test app=entity-front-matter
entity DocPage {
  [FrontMatter("id")] [MaxLength(200)] string Slug;
  [FrontMatter] [MaxLength(200)] string Title;
  [FrontMatter] string? Summary;
  [FrontMatter] [MaxLength(50)] string? Stability;
  [FrontMatter] [ForeignKey(Page)] DocTag[] Tags;
  [MaxLength(50)] string Area;
  Markdown Body;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

entity DocTag {
  DocPage Page;
  [MaxLength(80)] string Name;
  security { allow create, read, update, delete when IsAuthenticated || IsAnonymous; }
}

// `Title` and `Slug` are required, so every document must carry `title:` and `id:`.
void Publish(string area, string markdown) {
  new DocPage { Area = area, Body = markdown };
}

DocPage[] Preview() => DocPage.Where(p => p.Stability == "preview").ToList();

// `tags: [ui, …]` became rows, so a facet is an ordinary query.
DocPage[] Tagged(string tag) => DocPage.Where(p => p.Tags.Any(t => t.Name == tag)).ToList();
```

## See also       {#see-also}
- [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) — rendering the document the header belongs to.
- [entity members](https://osysharp.com/reference/entity/properties/) — declaring members, and what optional means.
- [entity](https://osysharp.com/reference/entity/declaration/) — the entity the members live on.
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — querying by a bound member, like any other.


---

<!-- https://osysharp.com/reference/entity/constraints/ -->

# constraints

> The per-member rules the database enforces — Required, Unique, MaxLength/MinLength, Min/Max, Pattern, and the storage-shaping Immutable, Precision and MaxBytes. They are checked when the row is written, so a bad row cannot reach the database from any code path.

<!-- id: entity-constraints · area: entity · stability: stable · html: https://osysharp.com/reference/entity/constraints/ -->

## Summary        {#summary}
Constraints are per-member rules enforced **when the row is written**. They are not form validation you can forget to
call: a row that breaks one cannot reach the database, whatever code path tried to write it — a function, an import,
a test, an API call.

## Signature      {#signature}
```osy title="the per-member rules, and where each one goes" syntax
[Unique(<Member>, <Member>)]                      // …or over a COMBINATION, declared on the entity
entity <Name> {
  [Required]                 <Type>   <Member>;   // must be present
  [Unique]                   string   <Member>;   // no two rows may share a value
  [MaxLength(<n>)]           string   <Member>;   // bounded text
  [Min(<n>), Max(<n>)]       int      <Member>;   // an inclusive numeric range
  [Pattern("<regex>")]       string   <Member>;   // must match
}
```

Every constraint takes an optional **message** as its last argument — what the user is told when the rule refuses
them:

```osy title="giving a constraint the message the user is told" syntax
  [Pattern("<regex>", "<message>")]  string  <Member>;
  [MaxLength(<n>, "<message>")]      string  <Member>;
  [Required("<message>")]            <Type>  <Member>;
  [Unique("<message>")]              string  <Member>;   // shown when the value collides with another row
```

…including the **entity-level composite** form, where the message goes last, after the members:

```osy title="a message on a combination — the members first, the sentence last" syntax
[Unique(<Member>, <Member>, "<message>")]
entity <Name> { … }
```

`[Unique]` is the one whose default wording helps least — a collision otherwise surfaces as the raw index name — so
its message is worth writing: `[Unique("That slug is already taken.")]`.

## Description    {#description}

### Which constraint attributes are there?   {#the-constraints}

| Attribute | Rule | On a null value |
|---|---|---|
| `[Required]` | the member must be present when the row is written | **this is the one that rejects null** |
| `[Unique]` | no two rows may hold the same value | passes — nulls do not collide |
| `[MaxLength(n)]` · `[MinLength(n)]` | text is at most / at least `n` characters | passes |
| `[Min(n)]` · `[Max(n)]` | an **inclusive** numeric range | passes |
| `[Pattern("…")]` | text matches the regular expression | passes |
| `[Immutable]` | may be set on create, but **never updated** | passes (it governs updates, not presence) |
| `[Precision(p, s)]` | a decimal stored with `p` total digits and `s` after the point | passes |
| `[MaxBytes(n)]` | the value's stored size is at most `n` bytes | passes |

Read that last column carefully: **every constraint except `[Required]` lets null through.** That is the correct
behaviour — "if there is a value, it must look like this" is a different rule from "there must be a value" — but it
surprises people. If a code must be present *and* well-formed, say both:

```osy title="present AND well-formed needs both" test app=entity-constraints
entity Product {
  [Required, Pattern("^[A-Z]{3}$")] string Code;   // must exist, and must be three capitals
  [Pattern("^[A-Z]{3}$")] string AltCode;          // may be absent; if present, must match
}
```

**Give a `[Pattern]` a message.** Every other constraint refuses in words a person can act on — *"Email is required"*,
*"Name is too long"*. A pattern refuses with the regular expression, which explains nothing to the person reading it:

```osy title="say what the shape means" test app=entity-constraints-message
entity Product {
  [Required, Pattern("^[A-Z]{3}$", "must be three capital letters")] string Code;
}
```

### Can I stack several on one member?   {#combining}
Attributes stack, in one bracket or several — whichever reads better:

```osy title="the full constraint set" test app=entity-constraints
entity Coupon {
  [Required, Unique, MaxLength(20)] string Code;   // present, one of a kind, bounded
  [Min(1), Max(100)] int PercentOff;               // inclusive: 1 and 100 both pass
  [MaxLength(500)] string Notes;                   // optional, but bounded when given
}
```

### `[Required]` on a reference forbids an orphan   {#required-reference}
`[Required]` works on a [reference](https://osysharp.com/reference/entity/relations/) too, and it is how you say "this child cannot exist without
its parent":

```osy title="a child that cannot be orphaned" test app=entity-constraints
entity Order {
  [Required] string Code;
}

entity LineItem {
  [Required] Order Order;   // a line with no order is a violation, not a stray row
  decimal Amount;
}
```

### `[Unique]` is enforced by the database   {#unique}
`[Unique]` is a real unique index, not a check-then-insert in application code. That distinction matters under
concurrency: two requests racing to claim the same coupon code cannot both win, because the second one is rejected by
the database rather than by a check that already passed.

⛔ **AND A SWAP BETWEEN TWO EXISTING ROWS IS REFUSED — the whole commit rolls back, silently.** This is the one
interaction an ordered list actually performs, and it is the opposite of what most people assume, so it is worth
stating plainly:

```osy syntax title="this does not work, and nothing on screen says why"
[Unique] int Position;
// …then, in an action:
int mine = job.Position;
job.Position  = above.Position;   // ← both rows now hold the same value for an instant
above.Position = mine;
UnitOfWork.Commit();              // ← the index refuses; NOTHING is written
```

A unique INDEX is checked per statement, not at commit, so the intermediate state where two rows share a value is
rejected even though the state you were committing is legal. **Measured**: the two rows come back unchanged, and
before 2026-08-29 the refusal reached nobody — no error, no banner, no fault in the test. It now surfaces in a
failing test as `action 'MoveUp' failed: Constraint violation: 'Job.Position' must be unique`, but the write still
does not land.

**So for a position column, do one of these:**

| | |
|---|---|
| **leave it un-`[Unique]`** | the ordinary answer. A rank has no meaning in a gap or a repeat beyond "which comes first", and nothing else depends on it being distinct. |
| **write the midpoint instead of swapping** | `job.Position = (above.Position + below.Position) / 2m` over a `decimal` — one row changes, so no two rows ever collide. This also stops a move rewriting the whole tail. |

⚠ **`[Unique]` is not deferrable today, and that is why.** Foreign keys are (they are emitted as deferrable
constraints); `[Unique]` is emitted as a `CREATE UNIQUE INDEX`, and Postgres cannot defer an index. Making it
deferrable would make the swap above work as written — it is a real option and it is not built.

**Over a COMBINATION of members, write it on the entity** — `[Unique(A, B)]` above the declaration, naming two or
more members. It is the same real unique index, over the pair — **and it takes the same optional message, written
last**: `[Unique(A, B, "…")]`. That is one form, not two, and it is the one to reach for. Without the message the
person is shown the index's own words; with it, they are told what they did:

```osy title="one membership per person per channel, and what a second one is told" test app=entity-unique-composite
entity Channel {
  [Required, MaxLength(60)] string Name;
}

[Unique(Channel, Person, "They are already in this channel.")]
entity Membership {
  [Required] Channel Channel;
  [Required] User Person;
}

[Principal] entity User {
  [Required, MaxLength(200)] string Email;
}
```

This is the constraint a **join table** wants, and reaching for a `Membership.Any(…)` guard instead is the mistake the
paragraph above describes: two requests can both read "not a member" before either writes, and only the index is
enforced where that race is. Keep the guard as well if you want a quiet no-op on a double-click — but it is the
convenience, not the rule.

**Which field does the violation land on?** For a composite one, a field whose name is **the members joined with
`", "`, in declaration order** — `Channel, Person` for the `Membership` above. Not either member on its own: the rule
is about the combination, so what is refused is the combination, named as one thing. That is what a test aims at, and
it is the one string you need:

```osy syntax title="aiming a test at the pair, not at either member"
Assert.Violation("Channel, Person");                                // the pair collided
Assert.Violation("Channel, Person", "already in this channel");     // …and this is what it says
```

A single-member `[Unique]` is the ordinary case — the field is just the member. Either way the violation is raised
by the **server**, at the save, because "is this taken?" is a question about other rows; see
[[testing-ui#composite-unique]] for when a test may assert it.

⚠ A unique constraint is over a **table**, and one table holds a whole [inheritance](https://osysharp.com/reference/entity/inheritance/) hierarchy —
so one declared on a base spans every type derived from it. That is usually what you want, and the compiler says so
either way (it warns, naming every type covered).

### Write-once, decimal precision, and size bounds   {#storage-shaping}
Four constraints shape a value beyond "is it valid":

- **`[Immutable]`** is a rule about *updates*, not presence: the member may be set when the row is created and then
  **never changed**. It is how you say "an order's placed-date is written once" — an attempt to update it is refused,
  from any code path. Pair it with `[Required]` when the value must also be present from the start.
- **`[MinLength]`** is the floor to `[MaxLength]`'s ceiling: `[MinLength(n)]` requires text of at least `n` characters.
- **`[Precision]`** pins how a `decimal` is stored: `[Precision(p, s)]` gives `p` total significant digits, `s` of them
  after the decimal point. `[Precision(10, 2)]` is the shape of money — up to eight digits before the point, two after.
- **`[MaxBytes]`** bounds the stored size of a value in bytes: `[MaxBytes(n)]` caps a large field — a `Json` document or
  rich text — where the limit you care about is storage, not character count.

```osy title="immutable, precision, and a byte bound" test app=entity-constraints
entity Invoice {
  [Required, Immutable] DateTime IssuedAt;      // set once, at creation; never edited afterwards
  [Precision(10, 2)] decimal Amount;            // money: 8 digits before the point, 2 after
  [MinLength(3), MaxLength(20)] string Number;  // a bounded reference code
  [MaxBytes(1048576)] Json Payload;             // at most 1 MB of stored JSON
}
```

### The rule spans several members — what then?   {#beyond-one-member}
A constraint speaks about **one member**. When the rule spans several — "shelf days must be under 30, but only for
perishable items" — you want an [invariant](https://osysharp.com/reference/entity/invariants/).

## See also       {#see-also}
- [invariant](https://osysharp.com/reference/entity/invariants/) — rules that span several members of the row
- [entity members](https://osysharp.com/reference/entity/properties/) — the members these constrain
- [relations](https://osysharp.com/reference/entity/relations/) — `[Required]` on a reference


---

<!-- https://osysharp.com/reference/entity/declaration/ -->

# entity

> Declares a persisted type — a table of rows the app stores, queries and secures. Every entity gets an Id and audit columns from the platform. Use a class instead when the value only lives in memory.

<!-- id: entity-declaration · area: entity · stability: stable · html: https://osysharp.com/reference/entity/declaration/ -->

## Summary        {#summary}
An `entity` is a **persisted type**: a table of rows the app stores, queries and secures. It is the noun your
application is about — `Order`, `Customer`, `Invoice`. Declaring one is enough to get a table, an identity, an audit
trail and a query surface; you write the members, the platform provides the rest.

If the value never needs to be stored — a computed shape, a DTO, the result of a calculation — use a
[`class`](https://osysharp.com/reference/class/constructors/) instead. A class lives in memory and has no table.

## Signature      {#signature}
```osy syntax
entity <Name> {
  <attributes> <Type> <Member>;    // data
  invariant <condition>;           // row-level rules
  security { … }                   // who may read and write it
}
```

## Description    {#description}

### Which members do I get for free?   {#provided}
Every entity already has these. **Do not declare them** — a member of the same name is a compile error, because the
platform is already providing one:

| Member | Type | What it is |
|---|---|---|
| `Id` | `Guid` | the row's identity, assigned on create |
| `CreatedAt` · `ModifiedAt` | `DateTime` | when the row was written |
| `CreatedBy` · `ModifiedBy` | `Guid` | who wrote it |

So an entity with two declared members has seven columns. You read them like any other member — `order.CreatedAt`
works without you writing it.

### How do I create a row, and when is it saved?   {#lifecycle}
A row is created with `new`, exactly as in C#. It is persisted when the function commits — there is no `Save()` call
to forget:

```osy title="an entity, and a function that makes one" test app=entity-declaration
entity Customer {
  [Required] string Name;
  string Email;
}

void Register(string name, string email) {
  var c = new Customer { Name = name, Email = email };
  // no Save() — the row is written when the function commits
}
```

Reading is a query over the type itself — you name the entity and write LINQ against it:

```osy title="reading rows back" test app=entity-declaration
int ActiveCustomers() {
  return Customer.Where(c => c.Email != null).ToList().Count;
}

Customer ByName(string name) {
  return Customer.Single(c => c.Name == name);   // exactly one, or it faults
}
```

The predicate is **compiled into the database query** — it is not a filter over rows you already fetched — so the
shape of what you can ask for is worth knowing before you write your first loop:

| I want to… | Read |
|---|---|
| filter, and pick one row or count them | [Where / Single / Count](https://osysharp.com/reference/query/where/) — `Where` · `Single` · `FirstOrDefault` · `Count` · `Any` |
| run the query and get the rows | [ToList](https://osysharp.com/reference/query/tolist/) — `ToList`, and why you should call it once |
| show page 3 of 40 | [Skip / Take (paging)](https://osysharp.com/reference/query/paging/) — `OrderBy` · `Skip` · `Take` |
| drop duplicates | [Distinct](https://osysharp.com/reference/query/distinct/) — and why it does nothing on whole rows |
| combine two result sets | [Union / Concat / Intersect / Except](https://osysharp.com/reference/query/set-operators/) — `Union` · `Concat` · `Intersect` · `Except` |
| match against a list I have in hand | [Dynamic IN (list.Contains in a query)](https://osysharp.com/reference/query/dynamic-in/) — `list.Contains(x)` inside a query |
| walk a parent's children | [relations](https://osysharp.com/reference/entity/relations/) — through the collection, **never** a filtered query |

Reaching for a `foreach` to do work the query could have done is the most common way an application that was fast on
a laptop becomes slow in production.

### Entity or class?   {#entity-vs-class}
| | `entity` | `class` |
|---|---|---|
| Stored in the database | **yes** — it has a table | no — in memory only |
| Has an `Id` and audit columns | **yes**, from the platform | no; it has only what you declare |
| Queryable (`Where`, `Single`, …) | **yes** | no |
| Securable (`security { … }`) | **yes** | no — it holds no rows to protect |
| Good for | the nouns you persist | DTOs, computed shapes, JSON bodies |

The rule of thumb: **if you will ever query it, it is an entity.**

### Security: an entity nobody can read is the DEFAULT   {#security}
This is the part to read twice, because it is where the platform will surprise you — deliberately.

**The posture is deny-all.** An entity that declares no `security { }` block is **denied to every user request**. Not
"readable by signed-in users", not "readable by its owner" — denied. Your beautifully modelled `Order` entity is, until
you say otherwise, an entity that no user of your application can read, write or count.

That is the correct default, and it is chosen on purpose. The failure mode of forgetting a rule is *"nobody can do
it"* — which someone reports within the minute — rather than *"everybody can"*, which nobody reports until it is
somebody else's headline. A door that fails shut is a door you can trust.

So **declaring who may read an entity is part of declaring the entity.** It is not a hardening pass you schedule for
later; there is no working application before you have done it.

```osy title="an entity only its owner can read" test app=entity-declaration
[Principal] entity User {
  [Required] string Name;
}

entity Note {
  User Owner;
  string Body;
  security { allow read where Owner == user; }
}
```

Note what the block does **not** contain: there is no `default deny`. Everything is denied already, so a
`security { }` block is a list of **grants** — you only ever write what is allowed. (To lock an entity completely,
write no block at all.)

Two rules, two different questions, and the distinction is the whole model:
- **`where`** filters by the **row** — *the owner sees their own notes*.
- **`when`** gates by the **principal** — *staff see every note*.

And note what you cannot do: a bare `allow read;` is a **compile error** on an app that has a principal. The platform
makes you say *who* may read — `allow read when IsAuthenticated;`, or `IsAuthenticated || IsAnonymous` if you really
do mean the whole internet. Opening a door is allowed; opening one by accident is not.

Read these three, in this order, before you ship anything:
1. [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — the deny-all posture, and what it does and does not cover
2. [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block in full: `where`, `when`, and named `policy` predicates
3. [runas](https://osysharp.com/reference/testing/runas/) — because **a security rule you have not tested is a rule you only believe you wrote**

That last one is not a flourish. Security is the one area where the code compiling, the tests passing and the feature
working tell you nothing about whether it is correct — the only way to know that Bob cannot read Alice's note is to
become Bob and try.

## See also       {#see-also}
- [entity members](https://osysharp.com/reference/entity/properties/) — the member types an entity can hold
- [relations](https://osysharp.com/reference/entity/relations/) — pointing one entity at another, and reading the children back
- [constraints](https://osysharp.com/reference/entity/constraints/) — `[Required]`, `[Unique]`, `[MaxLength]`, ranges and patterns
- [invariant](https://osysharp.com/reference/entity/invariants/) — row-level rules enforced when the row is written
- [constructor](https://osysharp.com/reference/class/constructors/) — the in-memory counterpart, for values you never store
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — the deny-all posture: what an entity permits before you say anything
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block in full
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — querying the rows, and the rest of the LINQ surface
- [runas](https://osysharp.com/reference/testing/runas/) — proving your rules deny the people they should


---

<!-- https://osysharp.com/reference/entity/inheritance/ -->

# entity Sub : Base

> Derives one entity from another. The subtype is its own type with its own name, its own security and its own workflows, and it carries every member of its base. Both store their rows in one table.

<!-- id: entity-inheritance · area: entity · stability: stable · html: https://osysharp.com/reference/entity/inheritance/ -->

## Summary        {#summary}
`entity RushOrder : Order` derives one entity from another, with C#'s colon and C#'s meaning. The subtype is its
**own type** — its own name, its own security rules, its own workflows — and it carries **every member of its base**
plus whatever it adds. Both types store their rows in **one table**.

Those two facts are the whole feature, and everything below is a consequence of one or the other.

## Signature      {#signature}
```osy syntax
entity <Sub> : <Base> {
  <attributes> <Type> <Member>;    // members of its OWN, on top of everything Base declares
  invariant <condition>;           // its own checks; Base's still apply
  security { … }                   // its OWN rules — nothing is inherited here
}
```

## Description    {#description}

### What a subtype is                    {#what}
A subtype is a **distinct type**. It has its own name, and everything the platform keys on a type keys on it
separately: its security rules are its own, a workflow that tracks it tracks only it, and a row of it reads back as
itself.

It is also **complete**. You do not reach through a base to get at inherited members — `RushOrder` simply *has*
`Reference`, in a query, in a UI binding, in a function:

```osy title="a base, a subtype, and both of their members" test app=entity-inheritance
entity Order {
  [Required, MaxLength(120)] string Reference;
  int Quantity;
}

entity RushOrder : Order {
  [Required, MaxLength(60)] string Courier;
}

void Ship() {
  // `Reference` and `Quantity` come from Order; `Courier` is RushOrder's own. No difference at the use site.
  var rush = new RushOrder { Reference = "R-1001", Quantity = 2, Courier = "DHL" };
}
```

### Where are a subtype's rows stored?        {#storage}
A subtype's rows live in its **base's table**, alongside the base's own. A hidden platform-written column records
which type each row is.

This is not an implementation detail you can ignore, because it is what makes the useful things possible: a
reference typed `Order` can hold a `RushOrder`, a parent-child tree can span types, and there is **one id space**, so
nothing has to ask "which table is this id in". It is also why a subtype may not redeclare a base member — see
[One column, so no redeclaring](#one-column) — and why `[Unique]` reaches further than you might expect — see
[Unique spans the hierarchy](#unique).

One consequence is worth knowing if you ever look at the table directly: **a member only a subtype declares is
optional in the database**, because rows of every other type genuinely have no value for it. `[Required]` is still
enforced — for that type, on every write — but the column itself holds NULL for everyone else, rather than a
fabricated blank.

### A subtype goes wherever its base is wanted   {#upcast}
A `SignedContract` **is** a `Document`, so it goes anywhere a `Document` is expected — a member, a function
argument — with no cast, exactly as in C#. This is what one table and one id space buy: a single reference column
holds any kind, and nothing at the far end knows the hierarchy exists.

```osy title="one FK, every kind of document" test app=entity-inheritance-upcast
entity Document { [Required, MaxLength(200)] string Title; }
entity Contract : Document { [Required, MaxLength(80)] string Counterparty; }

entity Note {
  [Required] Document Document;                 // typed as the ROOT…
  [Required, MaxLength(400)] string Body;
}

void Annotate() {
  var c = new Contract { Title = "Supply", Counterparty = "Acme" };
  new Note { Document = c, Body = "countersigned" };   // …and a Contract goes straight in
}
```

"Anywhere a `Document` is expected" includes the places where two values have to agree on one type — a `?:`, a
`switch` expression, a `??` fallback. The result is the **base** of the two, so `Document d = rush ? contract : doc;`
is the ordinary way to pick between them. Two SIBLINGS — a `Contract` and an `Invoice` — have no common type to
*infer*, so they take the type they are written INTO: `Document d = rush ? contract : invoice;` is fine, while
`var d = …` has nothing to take and says so.

The other direction — treating a `Document` you are holding as a `Contract` — can fail at run time, so you state it:
[test the row](#narrowing) with `is`, or narrow a whole set with `OfType<T>()`.

### Reading a type returns everything below it   {#reads}
`Order.Where(…)` returns rush orders too, because a `RushOrder` **is** an `Order` — the same thing a `List<Order>`
means in C#. **Narrowing is what you state:** `RushOrder.Where(…)` returns rush orders and whatever derives from
them, and never a plain `Order`. `Count()`, a collection you navigate to and a tree you walk all read the same way.

Each row that comes back is governed by [its own type's rules](#security), never by the type you asked through — so
a base read can return fewer rows than the table holds, and that is the rules working rather than a missing row.

```osy title="a base read returns every kind below it" test app=entity-inheritance
int AllOrders() {
  // The rush orders too — they are orders.
  return Order.Where(o => o.Quantity > 0).ToList().Count;
}

int RushOnly() {
  return RushOrder.Where(o => o.Quantity > 0).ToList().Count;
}
```

### How do I get back just one kind? — `is` and `OfType<T>`      {#narrowing}

A base read hands you every kind. **`is` tests one row; `OfType<T>()` narrows a whole set** — and both are
polymorphic downward, because a `SignedContract` **is** a `Contract`.

`x is Contract` is a plain condition: it works in a query, where it becomes a check the database does without
reading any rows, and in ordinary code. `is not Contract` is its negation.

```osy title="test one row, narrow one set" test app=entity-inheritance-narrow
entity Document { [Required, MaxLength(200)] string Title; }
entity Contract : Document { [Required, MaxLength(80)] string Counterparty; }
entity SignedContract : Contract { [Required, MaxLength(60)] string Signatory; }

// In a query — the database answers it; nothing is loaded to decide.
int ContractCount() { return Document.Where(d => d is Contract).ToList().Count; }

// `OfType<T>()` gives you a set of that type, so its own members are readable.
string Counterparties() {
  var all = "";
  foreach (var c in Document.OfType<Contract>().ToList()) { all = all + c.Counterparty + ";"; }
  return all;
}
```

Both count the `SignedContract` too. To ask for *only* the leaf, name it: `Document.OfType<SignedContract>()`.

#### Why can't I read the subtype's members after `is`?               {#narrowing-pattern}

`is` on its own answers a question; it does not change what you may read. `d is Contract` tells you the row is a
contract, and `d` is still a `Document`, so `d.Counterparty` does not compile. Give the test a **name** and it does:

```osy title="one list, one loop, rendered per kind" test app=entity-inheritance-narrow2
entity Document { [Required, MaxLength(200)] string Title; }
entity Contract : Document { [Required, MaxLength(80)] string Counterparty; }
entity Memo : Document { }

string Render() {
  var lines = "";
  foreach (var d in Document.OrderBy(x => x.Title).ToList()) {
    if (d is Contract c) {
      lines = lines + d.Title + " with " + c.Counterparty + "\n";   // `c` is the same row, as a Contract
    } else {
      lines = lines + d.Title + "\n";
    }
  }
  return lines;
}
```

`c` is the row you tested — nothing is copied — and it exists **inside the `if` only**. That is deliberate: outside
the branch the test may not have held, and a name that reads a row as a kind it is not would be worse than no name.

For the same reason these are refused, each with a sentence saying what to write instead:

| Written | Why |
|---|---|
| `is not Contract c` | the test *failing* says nothing about what `c` would be |
| `var b = d is Contract c;` | `c` belongs to a branch, and there is no branch here — use `is Contract` to get the answer |
| `(Contract)d` | a written downcast must fail at run time when the row is not one, and that check is not built |

### Security is never inherited          {#security}
A subtype declares its **own** `security { }` block, and inherits nothing from its base.

That is deliberate, and it is the safe direction. An entity with no policy grants nothing, so a subtype whose author
has not yet thought about who may read it returns **no rows** — rather than silently receiving whatever grant its
base happened to have. A row is always governed by the rules of **its actual type**, never by the type you reached
it through.

```osy title="the subtype states its own rules" test app=entity-inheritance-security
[Role] enum AppRole { Authenticator, Member }

[Principal]
entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Document {
  [Required, MaxLength(200)] string Title;
  security { allow create, read when IsAuthenticated; }   // any signed-in user sees any Document
}

entity Contract : Document {
  [Required, MaxLength(80)] string Counterparty;
  // Its OWN, and narrower. Nothing of Document's grant carries over — had this block been left out entirely, a
  // Contract would be readable by nobody, which is the direction you want to be wrong in.
  security { allow create, read where CreatedBy == user.Id; }
}
```

### What IS inherited                    {#inherited}

| Declaration | Inherited? | Why |
|---|---|---|
| members, and their attributes (`[Required]`, `[MaxLength]`, `[Searchable]`, …) | **yes** | they travel with the member, which the subtype has |
| `semantic => …` (the search card) | **yes**, and a subtype may declare its own to override | it is a *description*, and a subtype that adds nothing genuinely describes itself the same way |
| depth | **unlimited** — `SignedContract : Contract : Document` carries every member of both levels above it | |
| `invariant` | **yes**, and the subtype's own add to them | it is a *constraint*, and constraints only accumulate — a subtype cannot drop one |
| `security { }` | **no** — always its own | it is an *authority* question, where silence must mean no |

Description defaults to inheritance; authority defaults to denial.

### Can I redeclare a base's member on a subtype?        {#one-column}
Because both types share a table, a member declared on both is not a shadowed member — it is **one column claimed by
two declarations**. C# lets you shadow with `new`; a table cannot, so there is no spelling for it and the compiler
asks you to delete one:

```osy title="✗ a subtype redeclaring a base member — one column, two claims" syntax
entity Order    { [Required] string Reference; }
entity RushOrder : Order {
  [Required] string Reference;   // ✗ 'RushOrder.Reference' redeclares 'Order.Reference'
}
```

The realistic way to hit this is not a typo — it is a base gaining a member later that a subtype already used. The
error names both declarations so you can see which one you meant.

**Two SIBLINGS may share a member name, as long as they agree about it.** `Rush.Note` and `Standby.Note` do not
collide with each other the way a subtype collides with its base — they are different rows — so one shared column
serves both:

```osy title="siblings sharing a name, agreeing about it" test app=entity-inheritance-siblings
entity Ticket { [Required, MaxLength(200)] string Title; }

entity Bug      : Ticket { [MaxLength(80)] string Area; }
entity Feature  : Ticket { [MaxLength(80)] string Area; }   // same column, same meaning — fine
```

What is refused is the two **disagreeing**, because there is only one column and only one of them can win:

```osy title="✗ siblings disagreeing about the shared column" syntax
entity Bug     : Ticket { [MaxLength(80)] string Area; }
entity Feature : Ticket { int Area; }    // ✗ one column, declared as `string` by one type and `int` by the other
```

### `[Unique]` spans the hierarchy       {#unique}
A unique constraint is over a **table**, and one table holds every type in the hierarchy. So `[Unique]` on a base is
unique across the base *and every type derived from it*:

```osy title="one code, across every kind of order" test app=entity-inheritance-unique
entity Order {
  [Required, Unique, MaxLength(40)] string Code;   // no two Orders share a Code…
}

entity RushOrder : Order {                          // …and a RushOrder is an Order, so it is in the same space
  [Required, MaxLength(60)] string Courier;
}
```

An `Order` with `Code = "A-1"` and a `RushOrder` with `Code = "A-1"` is refused. That is usually exactly what you
want — one id space is much of the point of sharing a table — but it is impossible to read off the declaration, and
the other reading ("unique among Orders") is equally plausible. **So the compiler warns**, naming every type the
constraint covers:

> `'Order.Code' is unique across EVERY type stored in 'Order's table — Order, RushOrder — not just 'Order'.`

It is a warning rather than an error because the behaviour is correct; what was missing is that you were told. There
is no per-type spelling: if hierarchy-wide is not what you meant, the member belongs on one type rather than on a
shared one. The same warning appears for a composite `[Unique(A, B)]` and for a `[Unique]` a subtype declares.

### What happens to the rows if I remove a subtype?                   {#removing}
Deleting a subtype from your source means the same thing as deleting any other entity: **its rows go, and nobody
else's do.** It does not drop the shared table, the base and its siblings are untouched, and the columns that only
the removed type declared go with it.

Like every other drop it is gated — a plain recompile reports it and keeps the type, and removing it for real needs
`--prune` in development or an acknowledged migration in production.

Deleting a **base** while something still derives from it is a compile error: the subtype's `: Base` names a type
your application no longer declares. Remove the subtype in the same change, or keep the base.

### What is refused                      {#refused}

| Written | Refused because |
|---|---|
| a `sealed` base | the type said no type may derive from it — see [sealed](https://osysharp.com/reference/entity/sealed/) |
| two bases (`: A, B`) | Osy# has no interfaces, so a second name could only be a second base, and a row has one type |
| `class X : Y` | a class is an in-memory value, not a table; give it a field of the other type and compose |
| an entity deriving from a class, or the reverse | different kinds — one is a table, one is not |
| `extends` / `implements` | other languages' spellings; Osy# keeps C#'s colon, one spelling per concept |
| two siblings declaring one member DIFFERENTLY | one column, and only one of the two declarations can win |
| a written DOWNCAST (`(Contract)d`) | it must fail at run time when the row is not one, and that check is not built — [narrow instead](#narrowing) with `is` or `OfType<T>()`, which cannot fail |
| a member other than `security { }` on a subtype of a **platform** type | it shares a table the platform owns and writes — see [below](#platform-types) |

### Deriving from a platform type        {#platform-types}
A type a capability brings in may be derived from when it is not `sealed` — today that is `AgentTask`, so an app can
give its own kind of agent work its own type, its own rules and its own process.

**A subtype of a platform type declares `security { }` and nothing else.** It shares a table the platform owns and
writes, so adding a column to it would reshape platform storage — the same rule a `partial entity` over a platform
type already follows. Your own types are unrestricted: a subtype of a type *you* declare adds whatever it likes.

```osy title="an app's own kind of task, with its own loop" test app=entity-inheritance-platform
using Osysharp.Agents;

[Principal] entity Person {
  [Required, MaxLength(255)] string Email;
  security { allow read when IsAuthenticated; }
}

// A distinct type sharing AgentTask's table — no new columns anywhere.
entity PersonalTask : AgentTask {
  security { allow read when IsAuthenticated; }
}

workflow PersonalTaskProcessor {
  Tracks  = PersonalTask.Status;
  Initial = Running;
  state Running { }
  state Waiting { }
  terminal success Completed { }
  terminal error   Failed { }
}

app.Agent = new AgentConfig { Loop = PersonalTaskProcessor };
```

The task rows an agent opens under that loop **are** `PersonalTask`s: a task's type is the type its loop tracks. A
plain agent with no loop, or one whose loop tracks `AgentTask` itself, still gets an `AgentTask`.

Rows of a platform type stay platform-written through the subtype — the new name grants no ability to create,
change or delete one that the base did not.

Its fields, references and child collections all come through: `personalTask.Children` reads the same relation
`AgentTask.Children` does, because it IS that relation — there is one table, one foreign key, and one relation over
it, whichever type you reach them through.

### Workflows bind one type              {#workflows}
`Tracks = RushOrder.Status` binds `RushOrder` and nothing else — a workflow over a base does **not** cover its
subtypes. That is what keeps *one state machine per column* true: a base's run and a subtype's run would otherwise
both drive one `Status` value on one row.

It is also what makes inheritance the natural way to give two kinds of thing two different processes: give each its
own type, and each type its own workflow.

## Examples       {#examples}

```osy title="two kinds of task, two processes" test app=entity-inheritance-tasks
enum TaskState { Open, Doing, Done }

entity WorkItem {
  [Required, MaxLength(200)] string Title;
  TaskState State = TaskState.Open;
}

// Its own type, so its own rules and its own process — and no new columns.
entity UrgentItem : WorkItem {
  [Required] DateTime DueBy;
}
```

## See also       {#see-also}
- [sealed](https://osysharp.com/reference/entity/sealed/) — how a type says no one may derive from it
- [entity](https://osysharp.com/reference/entity/declaration/) — what an `entity` is, and what the platform provides for free
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block a subtype must write for itself
- [invariant](https://osysharp.com/reference/entity/invariants/) — the row-level checks a subtype accumulates from its base
- [relations](https://osysharp.com/reference/entity/relations/) — a reference typed as a base holds any of its subtypes
- **`demo/entity-inheritance`** — the runnable demo: four kinds of document in one table, three levels deep, with per-type
  security you can see by signing in as three different people (`osy docs sample` does not ship it; it lives in the
  repo's `demo/` tree)


---

<!-- https://osysharp.com/reference/entity/properties/ -->

# entity members

> The typed members an entity holds — text, numbers, dates, booleans, Guids, enums and references. A member's type spelling carries its nullability: a bare `string`, enum, `DateTime` or `Guid` is REQUIRED, a bare value type reads its zero, a reference is optional, and `?` makes any member optional. See [[types-optional-and-required]].

<!-- id: entity-properties · area: entity · stability: stable · html: https://osysharp.com/reference/entity/properties/ -->

## Summary        {#summary}
An entity's members are declared C#-style — `decimal Total;` — one per line, with optional attributes in front. Each
becomes a column. Whether a member is **required or optional is carried by its type spelling**, not a separate keyword:
a bare `string`, enum, `DateTime` or `Guid` is **required**, a bare value type (`int`, `bool`, `decimal`) reads its
zero, a **reference is optional**, and the `?` suffix makes any member optional. The full model — and *when* a required
member is checked — is [Optional and required members](https://osysharp.com/reference/types/optional-and-required/); the essentials are below.

## Signature      {#signature}
```osy syntax
entity <Name> {
  <attributes>? <Type> <MemberName>;
}
```

## Description    {#description}

### Which types may a member hold?   {#types}

**This table is the whole storable set** — [Every type, in one list](https://osysharp.com/reference/types/vocabulary/) is the same list with the non-storable types (the
collections, `Action`/`Func`, the component-parameter wrappers) beside it. If a type is not here and you did not
declare it yourself, an entity cannot hold it.

| Type | Holds | Notes |
|---|---|---|
| `string` | text | give it a `[MaxLength(n)]` — see below |
| `int` · `long` | whole numbers | `long` for ids and counters past 2³¹ — [long](https://osysharp.com/reference/types/long/) |
| `double` | measurements | a binary float — **never money** |
| `decimal` | money and exact quantities | **use this for money**, never a floating type — [decimal](https://osysharp.com/reference/types/decimal/) |
| `bool` | true / false | |
| `DateTime` | a point in time | stored UTC — [DateTime](https://osysharp.com/reference/types/datetime/) |
| `DateOnly` | a calendar date, no time | a SQL `date` — [DateOnly and TimeOnly](https://osysharp.com/reference/types/date-and-time/) |
| `TimeOnly` | a time of day, no date | a SQL `time` — [DateOnly and TimeOnly](https://osysharp.com/reference/types/date-and-time/) |
| `TimeSpan` | a duration | [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/) |
| `Guid` | an identifier | |
| `Json` | an arbitrary JSON document | for genuinely open-shaped data — [Json](https://osysharp.com/reference/types/json/) |
| `Markdown` | a section-addressable markdown document | text plus a rendering contract — [Markdown](https://osysharp.com/reference/types/markdown/) |
| `RichText` | formatted prose | a rich-text document |
| `Vector` | an embedding, for similarity search | `[MaxLength(n)]` sets the dimensions — [[Searchable]](https://osysharp.com/reference/memory/searchable/) |
| `Zone` | an IANA time-zone token (`Europe/Stockholm`) | [Time zones — the Zone type and its operations](https://osysharp.com/reference/stdlib/zones/) |
| `Culture` | a BCP-47 culture token (`sv-SE`) | [Culture formatting — ToString(format, culture)](https://osysharp.com/reference/stdlib/culture-formatting/) |
| `byte[]` | binary | the one array form that is a scalar, not a collection |
| an `enum` you declared | a fixed set of values | see [enum](https://osysharp.com/reference/enum/declaration/) |
| another `entity` | a reference to one row | see [relations](https://osysharp.com/reference/entity/relations/) |
| `<Entity>[]` | the children pointing back at this row | see [relations](https://osysharp.com/reference/entity/relations/) |

```osy title="the scalar types" test app=entity-properties
entity Invoice {
  [MaxLength(40)] string Number;
  decimal Total;                 // money is ALWAYS decimal
  int LineCount;
  bool Paid;
  DateTime IssuedAt;
  Guid ExternalRef;
}
```

The date/time trio and `Json` are **ordinary columns like any other** — there is no conversion to write and no
`DateTime` to fall back on. Reach for `DateOnly` when the time of day is not a fact about the row (a birthday, an
invoice date, the day a slot is booked for), and `TimeOnly` when the date is not (opening hours, a daily cutoff):

```osy title="dates without times, times without dates, and open-shaped data" test app=entity-properties
entity Appointment {
  DateOnly Day;                  // a calendar date — no time of day to get wrong
  TimeOnly StartsAt;             // a time of day — no date attached
  TimeSpan Runs;                 // how long it lasts
  Json Extras;                   // genuinely open-shaped data, stored as a document
}

DateOnly WhenIsIt(Appointment a) { return a.Day; }   // and it reads back as itself
```

**A member may be named for its own type**, exactly as `Color Color;` is in C# — and it is the natural spelling for
the commonest fields (`Room Room`, `Status Status`, `Type Type`). No prefix, no suffix, no second word:

```osy title="a member named for its own type — C#'s `Color Color`" test app=entity-properties
enum Room { Kitchen, Bathroom, Bedroom }

entity Kiln {
  [Required, MaxLength(80)] string Name;
  [Required] Room Room;          // the member and its type share a name — legal, and the name to use
}

Room WhereIsIt(Kiln p) { return p.Room; }   // and it reads back with no ceremony
```

### Is this member required, or may it be null?   {#optional}
Whether a member is required is decided by **how you spell its type** — there is no separate keyword. What a *bare*
(non-`?`) member means depends on whether its type has a natural zero (full treatment: [Optional and required members](https://osysharp.com/reference/types/optional-and-required/)):

- A **bare `string`, enum, `DateTime`/`DateOnly`/`TimeOnly`, `Guid` or `Json`** — a type with **no honest zero** — is
  **required**: the platform invents no value for it, so you must supply one. (An enum is *not* silently defaulted to
  its first member — reordering the members would change the stored default — so a bare enum is required too.)
- A **bare value-type scalar** — `int`, `long`, `bool`, `decimal`, `TimeSpan` — reads its **zero** (`0`, `false`, `0m`)
  when unset, exactly as a C# field does. It is never null; declare it `int?` for a real "unset". (Because it is never
  null, comparing one to `null` — `priority == null` — is a compile error that points you at the `?` form.)
- A **bare entity reference is optional** (reads back `null`) — the everyday shape is *create the row, then pick the
  related record* — and you write `[Required]` to demand one.
- The **`?` suffix** makes any member optional; it reads back `null` when nobody set it.

**When is a required member checked?** An entity is a **draft until commit**, so `new Ticket {}` compiles — you seed an
empty draft, bind each field to a form input, and the required members are validated **at commit**, naming any that are
still unset. (A `class` has no commit step, so its required members are checked at `new` instead — see
[class properties](https://osysharp.com/reference/class/properties/).) This is what makes the everyday create-form work.

```osy title="required by spelling; optional with ?" test app=entity-properties
entity Contact {
  string Name;              // REQUIRED — a bare string has no honest zero (checked at commit)
  string? Phone;            // optional — a contact without a phone is fine; reads back null
  DateTime? LastSpokeAt;    // `?` is not a string thing: ANY type takes it, value types included
  int? Doorstep;            // `int?` is genuinely absent, which is what `0` could never say
}

string PhoneOrDash(Contact c) {
  return c.Phone ?? "—";    // null-coalescing on the optional field, exactly as in C#
}
```

⚑ **EVERY C# NULLABLE TYPE IS SUPPORTED.** `?` is not a string affordance — it is the C# rule, and it holds for
value types and reference types alike: `DateTime?`, `int?`, `decimal?`, `bool?`, `Guid?`, `TimeOnly?`, an enum,
your own `class`. There is no list to check against, which is why this page states the rule rather than
enumerating one.

⚠ **It is written down because the shape is usually only ever SHOWN on a string**, and a reader who has seen
`string?` and nothing else has to guess. The guess costs a field: measured 2026-09-01, a model reasoning aloud —
*"`DateTime?` — is nullable DateTime supported? The docs mention `string?` for nullable"* — and it dropped the
field rather than find out.

A bare **reference** is the exception — optional by default, so write `[Required]` to demand one:
`[Required] Customer Reporter;`. See [relations](https://osysharp.com/reference/entity/relations/) and [Optional and required members](https://osysharp.com/reference/types/optional-and-required/).

### How do I give a member a default value?   {#defaults}
A member may declare a default, which applies when the row is created without one — `= true`, `= 0m`, or an enum
member. It is the honest way to say *"this is what a new one looks like"*, instead of remembering to set it at every
creation site:

```osy title="what a new row looks like" test app=entity-properties
enum AccountStatus { Active, Suspended, Closed }

entity Account {
  [Required, MaxLength(200)] string Name;
  bool IsActive = true;                          // a new account is active
  AccountStatus Status = AccountStatus.Active;   // …and its status says so
  decimal Balance = 0m;
  [Required] string ApiKey = Security.RandomId(32);   // a fresh unguessable key per account
  DateTime CreatedFor = DateTime.UtcNow.AddDays(30);     // …and a computed date, evaluated at creation
}

void Open(string name) {
  var a = new Account { Name = name };
  // IsActive is true, Status is Active, Balance is 0 — none of them written here;
  // ApiKey is a fresh 32-char id and CreatedFor is 30 days out — each EVALUATED for this new row
}
```

A default is not limited to a constant. It can be **any expression** — a call like `Security.RandomId(32)`, a
computed value like `DateTime.UtcNow.AddDays(30)`, arithmetic — and it is **evaluated afresh for each row at creation**,
exactly like a C# field initializer. So two accounts opened in the same breath get two *different* `ApiKey`s; the
expression runs per row, not once. (A constant/enum default behaves the same as always.)

Reach for a default whenever "unset" and "the normal value" are the same thing. It removes a whole class of bug:
the creation site somebody added last week that forgot to set `IsActive`, and the row that has been invisible ever
since.

### Text needs a length   {#maxlength}
`string` with no `[MaxLength]` is unbounded. That is fine for a body of prose and wrong for a code, a name or a
status — give those a length, and the database enforces it:

```osy title="bounded and unbounded text" test app=entity-properties
entity Article {
  [MaxLength(200)] string Title;   // bounded — a title has a sane maximum
  string Body;                     // unbounded — prose
}
```

### Money is `decimal`   {#money}
There is no `float` or `double` member type, and that is on purpose: binary floating point cannot represent `0.10`,
so totals drift by fractions of a cent and eventually a customer notices. `decimal` is exact.

### How do I index a member, or rename its column?   {#storage-attributes}
A handful of attributes shape how a member is *stored* rather than what it may hold. Most rows never need them:

- **`[Index]`** asks the database to index the member, so queries that filter or sort by it stay fast as the table
  grows. Put it on the members you actually query by.
- **`[ExternalName]`** overrides the database column name when it must differ from the member — `[ExternalName("col")]`
  maps onto a pre-existing or externally-owned schema.
- **`[Virtual]`** marks a member **in-memory only**: it is never persisted, for a value you compute and carry but do
  not store.
- **`[Id]`** marks a member as the entity's primary key. Every entity already has an automatic `Id`, so you only reach
  for this to supply your own key instead of the default.
- **`[DynamicType]`** lets a member's type be resolved at runtime rather than fixed at declaration — an advanced escape
  hatch for genuinely polymorphic storage.
- **`[Mentions]`** declares the entities a rich-text member may `@`-mention, so an editor can offer and resolve them.

```osy title="indexing and an external column name" test app=entity-properties
entity LegacyCustomer {
  [Required, Index] string Status;              // queried often → indexed
  [ExternalName("cust_ref")] string Reference;  // the column is named cust_ref in the database
  [Virtual] int ScoreThisSession;               // computed and carried, never stored
}
```

## See also       {#see-also}
- [entity](https://osysharp.com/reference/entity/declaration/) — the type these members live in
- [constraints](https://osysharp.com/reference/entity/constraints/) — `[Required]`, `[Unique]`, `[MaxLength]`, `[Min]`/`[Max]`, `[Pattern]`
- [relations](https://osysharp.com/reference/entity/relations/) — members that point at another entity
- [enum](https://osysharp.com/reference/enum/declaration/) — a member with a fixed set of values


---

<!-- https://osysharp.com/reference/entity/invariants/ -->

# invariant

> A row-level rule spanning several members, checked when the row is written. Use it when a constraint on one member is not enough — a balance that may not go negative, or a rule that only applies to some rows.

<!-- id: entity-invariants · area: entity · stability: stable · html: https://osysharp.com/reference/entity/invariants/ -->

## Summary        {#summary}
An `invariant` is a rule about **the whole row**, enforced when the row is written. A
[constraint](https://osysharp.com/reference/entity/constraints/) speaks about one member (`[Max(5)] int Priority`); an invariant speaks about the
relationship between members — and can be conditional, so it applies only to the rows it should.

## Signature      {#signature}
```osy syntax
entity <Name> {
  invariant <condition>;                                       // always holds
  invariant <condition> when <guard>;                          // holds only for rows where <guard> is true
  invariant <condition> when <guard> message "<explanation>";  // …and say why, when it fails
}
```

## Description    {#description}

### How do I write a rule over the whole row?   {#always}
The condition is written over the row's own members, and must be true when the row is written:

```osy title="a balance that may never go negative" test app=entity-invariants
entity Account {
  [Required] string Holder;
  decimal Balance;
  invariant Balance >= 0;
}
```

Any code path that would leave `Balance` negative — a withdrawal, an import, an adjustment — fails at commit. You do
not have to remember to check it, and neither does the next person.

### A rule that applies to some rows   {#when}
`when` guards the invariant. The rule is only checked for rows where the guard is true, so one entity can hold
several kinds of row without the rules of one contaminating the others:

```osy title="a rule that only applies to perishable goods" test app=entity-invariants
entity Product {
  [Required] string Name;
  bool IsPerishable;
  int ShelfDays;

  invariant ShelfDays <= 30 when IsPerishable
    message "Perishable items can have at most 30 shelf days.";
}
```

A tin of beans with `ShelfDays = 400` is fine — the guard is false, so the rule never fires. Milk with the same value
is rejected, and the person who tried gets the sentence you wrote rather than a constraint name.

### How do I say why it failed? — `message`   {#message}
`message` is the explanation a human sees. Without it they get a rule that says `ShelfDays <= 30`, which tells them
what was violated but not what to do. Write the message as though you are talking to the person who hit it — because
you are.

### What an invariant can see   {#scope}
The row it is on. An invariant reads the members of its own entity; it does not run a query and it does not reach
across into other rows. That is what keeps it cheap enough to check on every write.

## See also       {#see-also}
- [constraints](https://osysharp.com/reference/entity/constraints/) — the single-member rules (`[Required]`, `[Unique]`, `[Min]`/`[Max]`, `[Pattern]`)
- [entity members](https://osysharp.com/reference/entity/properties/) — the members an invariant reads
- [entity](https://osysharp.com/reference/entity/declaration/) — the entity an invariant guards


---

<!-- https://osysharp.com/reference/entity/relations/ -->

# relations

> One entity points at another by declaring it as a member — that is the foreign key. The parent reads its children back through a collection member marked with ForeignKey. Never query children with a filter; go through the collection.

<!-- id: entity-relations · area: entity · stability: stable · html: https://osysharp.com/reference/entity/relations/ -->

## Summary        {#summary}
A relation is two halves of one idea. The **child** points up by declaring the parent as a member — `Order Order;` —
which is the foreign key. The **parent** reads down through a collection member — `[ForeignKey(Order)] LineItem[]
LineItems;`. Declare both, and you can walk the graph in either direction.

## Signature      {#signature}
```osy syntax
entity <Child> {
  [Required] <Parent> <Parent>;                    // the FK — one row of the parent
}

entity <Parent> {
  [ForeignKey(<Parent>)] <Child>[] <Children>;     // the children pointing back at this row
}
```

## Description    {#description}

### How do I declare both sides of a relation?   {#two-halves}
The child's member IS the foreign key — there is no `OrderId` to declare and keep in step. You assign a row, not an
id, and you read a row back. The parent's collection member carries the **`[ForeignKey]`** attribute, naming the
relation it reads down — `[ForeignKey(Order)]` on the `Order`'s `Lines`:

```osy title="an order and its lines" test app=entity-relations
entity Order {
  [Required] string Code;
  decimal Total;
  [ForeignKey(Order)] LineItem[] Lines;   // read down: this order's lines
}

entity LineItem {
  [Required] Order Order;                  // point up: the order this line belongs to
  [MaxLength(200)] string Product;
  decimal Amount;
}

void AddLine(string orderCode, string product, decimal amount) {
  var order = Order.Single(o => o.Code == orderCode);
  var line = new LineItem { Order = order, Product = product, Amount = amount };
  //                        ^^^^^^^^^^^^^ assign the ROW, not an id
}
```

`[Required]` on the child's parent member means an orphan is impossible: a `LineItem` with no `Order` is a violation
at commit, not a row nobody notices for six months.

### Read children through the collection, never a filter   {#never-query-children}
This is the one rule people get wrong. To get an order's lines, go through the collection:

```osy title="reading a parent's children" test app=entity-relations
decimal OrderTotal(string code) {
  var order = Order.Single(o => o.Code == code);
  var total = 0m;
  foreach (var line in order.Lines) {     // the collection — connected to the order you already loaded
    total += line.Amount;
  }
  return total;
}
```

Do **not** write a standalone query filtered on the foreign key to fetch children. It looks equivalent and is not:
the collection is part of the object graph you already have, so it is loaded once and reused, while a separate query
is disconnected from the parent and re-runs every time you touch it. The collection is the connected path; a filtered
query is a second, unrelated result set that happens to contain the same rows.

### How do I let a reference be unset?   {#optional}
Leave off `[Required]` and the reference may be null — a `Ticket` that nobody is assigned to yet:

```osy title="an optional reference" test app=entity-relations
entity Person {
  [Required] string Name;
}

entity Ticket {
  [Required] string Title;
  Person Assignee;                     // may be null — an unassigned ticket is a real state
}

string AssigneeName(Ticket t) {
  return t.Assignee?.Name ?? "unassigned";   // null-safe, exactly as in C#
}
```

### What `[Required]` decides about deleting   {#required-and-delete}
A required reference says the child **cannot exist without its parent**, and the platform takes that literally:
deleting the parent deletes those children with it. An optional reference is the other answer — deleting the parent
leaves the child and clears its reference.

```osy syntax
entity Tag {
  [Required] Note Note;    // deleting the Note deletes this Tag
}

entity Draft {
  Note Note;               // deleting the Note leaves this Draft, with Note cleared
}
```

⚠ **This is where the decision is made, so it is worth making deliberately.** A cascade removes the children
**without asking whether the caller could have deleted them on their own** — someone allowed to delete a `Note` can
delete its `Tag`s by deleting the note, even where your rules never grant them `delete` on `Tag`. That is the
declaration doing what it says rather than a gap in it: the alternative would be a legal model that fails at run
time, with nothing you could write to fix it.

So if a child's removal should be governed separately from its parent's, do not make its reference required —
model it as optional and delete it explicitly.

## See also       {#see-also}
- [entity](https://osysharp.com/reference/entity/declaration/) — the entities a relation joins
- [constraints](https://osysharp.com/reference/entity/constraints/) — `[Required]` on a reference is what forbids an orphan
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — querying the entities themselves


---

<!-- https://osysharp.com/reference/entity/sealed/ -->

# sealed

> Declares that no type may derive from this entity. A type is open unless it says otherwise, exactly as in C#. Sealing says nothing about who may read or write the type.

<!-- id: entity-sealed · area: entity · stability: stable · html: https://osysharp.com/reference/entity/sealed/ -->

## Summary        {#summary}
`sealed entity Invoice { … }` declares that **no type may derive from this one**. It is C#'s keyword with C#'s
default: a type is **open unless sealed**.

Sealing is a decision a type makes **about itself**. It has nothing to do with access — a sealed entity's rows are
read and written exactly as any other's, governed by its `security { }` block.

## Signature      {#signature}
```osy syntax
sealed entity <Name> { <members> }
```

## Description    {#description}

### What does `sealed` do?                        {#what}
An attempt to derive from a sealed type is a compile error, naming the sealed type:

```osy syntax
sealed entity Invoice { [Required] string Number; }
entity CreditNote : Invoice { … }   // ✗ 'Invoice' is `sealed`, so no type may derive from it
```

Seal a type when its shape and its rules are the whole story and a subtype would only blur them — a ledger entry, an
audit row, a settled document. Leave it open when deriving is a use you intend to support.

### What sealing is NOT — access control, `partial`, `class`  {#orthogonal}

- **Sealing is not access control.** It does not restrict reading or writing; that is the [`security { }`](https://osysharp.com/reference/security/entity-security/) block, and a sealed entity takes one exactly as any other does.
- **Sealing is compatible with `partial`.** `partial entity X { security { … } }` states more about *this* type; sealing refuses a *new* type. C# treats them as orthogonal for the same reason. (`sealed` **on** a `partial` is refused, though — a partial does not declare the type, so it cannot decide what may derive from it.)
- **A `class` is sealed already**, by fact rather than by declaration: nothing can derive from an in-memory value type in Osy#, so writing the word there would say nothing and is refused.

### The platform seals its own types     {#platform}
Every type the platform ships is sealed, with deliberate, reviewed exceptions for the ones you are meant to extend.
So if a platform type takes a `: Base` clause, that is a commitment rather than an oversight.

## Examples       {#examples}

```osy title="a settled document, and an open one" test app=entity-sealed
// Nothing derives from a posted ledger entry — its shape and its rules ARE the record.
sealed entity LedgerEntry {
  [Required, MaxLength(40)] string Reference;
  [Required] decimal Amount;
}

// A document, on the other hand, is a shape other kinds of document build on.
entity Document {
  [Required, MaxLength(200)] string Title;
}

entity Contract : Document {
  [Required, MaxLength(80)] string Counterparty;
}
```

## See also       {#see-also}
- [entity Sub : Base](https://osysharp.com/reference/entity/inheritance/) — what deriving actually gives you, and what it refuses
- [entity](https://osysharp.com/reference/entity/declaration/) — the `entity` declaration itself
- [security { }](https://osysharp.com/reference/security/entity-security/) — who may read and write a type, which sealing does not touch


---

<!-- https://osysharp.com/reference/enum/index/ -->

# Enums

> A fixed set of named values, used as a member type. By default an enum stores as a compact number; add [Type(string)] to store the member's own name instead — what you want when a human or another system reads the column. [Label] gives a member the human-readable label a screen shows. Three answers you will want before you open a page. (1) Room.Members is every member, in declaration order — `foreach (var r in Room.Members)` is how you build a picker, a tab bar or a filter; never hardcode the list, and Enum.GetValues<Room>() is the same array if you prefer C#'s spelling. (2) A member may be named for its own type: `[Required] Room Room;` compiles, exactly as `Color Color` does in C#. (3) Text(p.Room) renders the [Label] label, but "in " + p.Room concatenates the raw member NAME ("LivingRoom") — say p.Room.Label wherever a string is what you need. Full detail: osy docs enum-declaration#members and osy docs enum-labels#label-property.

<!-- id: enum-index · area: enum · stability: stable · html: https://osysharp.com/reference/enum/index/ -->

## Summary        {#summary}
An `enum` is a **fixed set of named values** — `enum Status { Draft, Placed, Cancelled }` — used as the type of a
member. It gives you a closed vocabulary the compiler checks: a value outside the set is a compile error, not a bad
row. Two small decisions round it out: **how it is stored** (a number, or its name) and **what a human sees** (its
label).

## Description    {#description}

### Declaring one   {#declaring}
An [enum](https://osysharp.com/reference/enum/declaration/) lists its members; any member or property can then take the enum as its type (an
`entity Order { Status Status; }`, a function local, a component prop). The compiler enforces the set everywhere the
enum is used, so a typo or a stale value is caught at compile time:

```osy title="a closed set the compiler checks" test app=enum-index
enum Status { Draft, Placed, Cancelled }
```

```osy title="proof: the members are distinct values" run app=enum-index
[Test]
void Status_is_a_fixed_set() {
  var s = Status.Placed;
  Assert.NotEqual(Status.Draft, s);
}
```

### Stored as a number, or as its name   {#storage}
By default an enum member is stored as a **number** — compact, and fine when only your own code reads it. When a
human or another system will read the column, store it as the member's **own name** with `[Type(string)]`, so the
value in the database is `"Placed"` rather than `1`. That is the setting to reach for on anything exported, reported
on, or read by an integration. See [enum](https://osysharp.com/reference/enum/declaration/).

### The label a human reads   {#labels}
A member's stored value is compact; the label on screen doesn't have to be. [[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/) — `[Label("…")]` — gives
a member a human-readable label (the member's own name is the default), and a doc comment gives it a longer
description. So `InProgress` can show as "In progress" without changing what is stored.

## See also       {#see-also}
- [enum](https://osysharp.com/reference/enum/declaration/) — declaring the set, and `[Type(string)]` for name storage
- [[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/) — `[Label]` and the human-facing label
- [Types](https://osysharp.com/reference/types/index/) — the rest of the type system


---

<!-- https://osysharp.com/reference/enum/labels/ -->

# [Label], [Icon], [Tone] — what a human reads

> An enum member stores a compact value but shows a human-readable label. [Label("…")] gives a member its label (otherwise the member's own name is used), and a doc comment gives it a longer description. A screen that shows an enum-typed value — a grid cell, a text line — reads the label, never the stored value. [Icon(…)] and [Tone(…)] say which icon and which tone stand for a member, so every screen shows it the same way without repeating the decision.

<!-- id: enum-labels · area: enum · stability: stable · html: https://osysharp.com/reference/enum/labels/ -->

## Summary        {#summary}
An enum member has two faces. It has a **stored value** — the compact thing that lives in the column and that your
code compares against — and it has a **label**, the words a person reads on screen. `[Label("…")]` sets the label.
Without one, the label is simply the member's name.

You never have to convert between them. Show an enum-typed value anywhere in a screen and the label is what appears.

## Signature      {#signature}
```osy syntax
enum <Name> {
  [Label("<label>")] <Member>,     // an explicit label

  /// <description>
  <Member>,                          // a doc comment becomes the member's description
}
```

## Description    {#description}

### How do I change the words a member shows?   {#label}
A member name is an identifier, so it cannot contain spaces or punctuation. `[Label]` supplies the words:

```osy title="labels a person can read" test app=enum-labels
enum OrganizationType {
  /// A single person's own space.
  Personal,

  [Label("Team or company")] Team,
}

entity Organization {
  [Required, MaxLength(100)] string Name;
  OrganizationType Type = OrganizationType.Team;
}
```

`Team` reads as **Team or company**. `Personal` declares no `[Label]`, so it reads as **Personal** — the member
name is a perfectly good label when it already says what it means, and you should not add `[Label("Personal")]`
just to be explicit.

A doc comment on a member becomes its **description** — a longer sentence a control can show beside the label, such
as the help text under an option in a dropdown. It is optional; a member with no doc comment simply has none.

### How do I show an enum value on a screen?   {#showing}
Anywhere a screen displays an enum-typed value, it displays the label:

```osy title="a list that reads as words" test app=enum-labels
[Page("/orgs")]
[Render(CSR)]
component OrganizationsPage() {
  var orgs = Organization.ToList();

  render {
    Stack(gap: 2) {
      foreach (var o in orgs) {
        Row(gap: 3) {
          Text(o.Name);
          Text(o.Type);        // "Team or company" — the label, not the stored value
        }
      }
    }
  }
}
```

A grid column reads the same way: give it the member's key (`Type`) and the cell shows the label. You never write a
lookup, a mapping, or a `switch` to turn a value into words.

Because the label is resolved only for display, the value itself is untouched — `o.Type == OrganizationType.Team`
still compares against the stored value, exactly as it always did.

### When you need the label as a STRING — `.Label`   {#label-property}
Displaying a value shows its label without being asked. But some props take a **string**, not a value to render — a
`Button`'s label, a `title`, an aria name — and there the label has to be read. `.Label` is that read.

**The difference is the SLOT, not the value** — one table so it never has to be worked out:

| you write | you get | why |
|---|---|---|
| `Text(dish.Cuisine)` | `Street food` — the label | a text slot RENDERS an enum value |
| `Badge(dish.Cuisine.Label, …)` | `Street food` | a `string` PARAMETER takes a string, so read the label |
| `Text("in " + dish.Cuisine)` | `in StreetFood` — the **member name** | `+` is a string op, and an enum's string form is its name (C#-exact) |
| `Text("in " + dish.Cuisine.Label)` | `in Street food` | …so say `.Label` when you concatenate |
| `Row { Text("in "); Text(dish.Cuisine); }` | `in Street food` | …or give the enum its own text slot |

⚑ The third row is the one that surprises, and it is not a compile error — it renders, just with the wrong words.
Two generated apps in a row reasoned their way to the last row from first principles rather than reading it here.

```osy title="a filter button per member, worded properly" test app=enum-label-property
enum Cuisine { [Label("Street food")] StreetFood, Thai, Nordic }

[Page("/cuisines")]
[AllowAnonymous]
component CuisineFilter() {
  Cuisine picked = Cuisine.Thai;
  action Pick(Cuisine c) { picked = c; }

  render {
    Row(gap: 2) {
      foreach (var c in Cuisine.Members) {
        Button(c.Label, onPress: () => Pick(c));   // "Street food", not "StreetFood"
      }
    }
  }
}
```

`Cuisine.Members` is every member of the enum, so a picker is a `foreach` and stays right when a member is added.
`.Description`, `.Name`, `.Icon` and `.Tone` read the rest of a member's presentation the same way — `.Name` is the
member's identifier (`"StreetFood"`), which is what `.ToString()` gives you and almost never what a person should
read.

⚠ They are **properties, not methods**: `r.Label`, never `r.Label()`.

### The stored string must differ from the name — `[Value]`   {#value}
Under `[Type(string)]` an enum member is stored by its **name** by default. When the stored string must differ from the
name — to match an external system, or to keep a stable code while the member is renamed — the **`[Value]`** attribute
sets it explicitly: `[Value("team")] Team` stores `"team"` while your code still writes `OrganizationType.Team`. Most
enums never need it; reach for it only when the storage string is a contract with something outside your app.

### And you read it back with `.Value`   {#read-the-value}
`[Value("…")]` is not write-only. **`.Value` on an enum-typed value is the stored key** — the string you declared,
or the member's own name where you declared none:

```osy title="the declared key, read back" test app=enum-value-readback
[Type(string)] enum TimeSlot {
  [Value("08:00")] Early,
  [Value("10:00")] Mid,
  Late,                          // no [Value] — stores its own name
}

string SlotStart(TimeSlot slot) { return slot.Value; }   // "08:00" / "10:00" / "Late"
```

It is the **fourth word** beside `.Label`, `.Description` and `.Name`, and it is the one to reach for when something
OUTSIDE your app has to receive the value you chose — a URL, a header, a row you are exporting. `.Label` is for a
person, `.Name` is the identifier you wrote, and `.Value` is what the column and the wire hold.

⚠ **Without `[Type(string)]` an enum stores its ordinal, and `.Value` answers that** — the `int`. One spelling,
whichever storage the enum declared.

⚠ **On an OPTIONAL enum, `.Value` still means unwrap**, exactly as in C#. So the key of a `TimeSlot?` is
`slot.Value.Value`: unwrap first, then read the key.

⭐ **Reach for this instead of writing the mapping out.** A hand-written `slot == TimeSlot.Early ? "08:00" : …`
restates the keys the enum already declares, and the two drift the first time somebody edits one of them — which is
the same reason [`<Enum>.Members`](https://osysharp.com/reference/enum/declaration/) exists.

### Which icon and tone stand for a member? — `[Icon]` and `[Tone]`   {#presentation}
A label is not the only thing a member has. "Cancelled" is usually also a **cross**, and usually also a **danger** —
and those are facts about the member, not about the screen that happens to be showing it. Say them once:

```osy title="a member carries its own icon and tone" test app=enum-labels
enum Tone { Neutral, Success, Warning, Danger, Accent }

enum OrderState {
  [Label("Active"),    Icon(check), Tone(Tone.Success)] Active,
  [Label("On hold"),   Icon(pause), Tone(Tone.Warning)] OnHold,
  [Label("Cancelled"), Icon(close), Tone(Tone.Danger)]  Cancelled,
}
```

Without this, every dropdown, cell, badge and header re-decides with its own `if (state == Cancelled)` chain — which
is how the same enum ends up red on one screen and grey on the next.

**`[Icon]` names a declared icon; `[Tone]` names a declared tone.** `Icon(check)` must name an icon your app can
draw — one it declares (an `icons/check.svg`) or one of the built-ins — written unquoted.

⚠ **Here the name is BARE, and at a call site it is qualified.** The attribute takes `Icon(check)`; rendering the
same glyph directly takes `Icon(Icons.Check)`. The two spellings are not interchangeable, and writing the qualified
form in the attribute is a compile error. `Tone(Tone.Danger)` is a **qualified reference to a member of a tone enum**
— the same shape as `[Classification(DataClass.PII)]` — so the referenced member is checked. A typo in either is a
compile error with a suggestion, not a blank space at runtime.

**`[Tone]` names a tone and never a colour.** `[Tone(Tone.Success)]` says the member *is* a success and lets the
design system decide what that looks like — so re-theming the app carries every enum with it, and a dark mode does not
need the enum edited. A hex here would put presentation in your domain model permanently.

Neither has a fallback. A member with no `[Icon]` has no icon, and a screen is free to show nothing — unlike the
label, where something must always be shown.

### Changing a label is safe; changing a name is not   {#renaming}
The label is presentation, so you can reword it freely — no stored data refers to it. The member **name**, by
contrast, is what your code names (`OrganizationType.Team`), and under `[Type(string)]` it is also what the column
stores (unless a `[Value]` pins it). Reword the label when the words are wrong; rename the member only when the concept
is.

## See also       {#see-also}
- [icons](https://osysharp.com/reference/ui/icons/) — declaring the icons `[Icon(…)]` may name
- [enum](https://osysharp.com/reference/enum/declaration/) — declaring an enum and choosing how it is stored
- [entity members](https://osysharp.com/reference/entity/properties/) — using an enum as a member type
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — controls that show and edit an entity's values


---

<!-- https://osysharp.com/reference/enum/declaration/ -->

# enum

> A fixed set of named values, used as a member type. Stored as a number by default, or as the member's own name with [Type(string)] — which is what you want when a human or another system will read the column.

<!-- id: enum-declaration · area: enum · stability: stable · html: https://osysharp.com/reference/enum/declaration/ -->

## Summary        {#summary}
An `enum` is a fixed set of named values — `Draft`, `Placed`, `Shipped`. Use one wherever a member has a known,
closed set of states. The compiler then knows every possible value, so a typo is an error and a
[`switch`](https://osysharp.com/reference/function/switch/) over it can be checked for completeness.

## Signature      {#signature}
```osy syntax
enum <Name> { <Member>, <Member>, … }

[Type(string)]                       // persist the member's NAME instead of a number
enum <Name> { <Member>, … }
```

## Description    {#description}

### How do I declare one and set it on a row?   {#using}
An enum is declared at the top level and used as a member type:

```osy title="a status enum" test app=enum-declaration
enum OrderStatus { Draft, Placed, Shipped, Cancelled }

entity Order {
  [Required] string Code;
  OrderStatus Status;
}

void Place(string code) {
  var o = Order.Single(x => x.Code == code);
  o.Status = OrderStatus.Placed;      // always qualified — OrderStatus.Placed, never "Placed"
}

int PlacedCount() {
  return Order.Where(o => o.Status == OrderStatus.Placed).ToList().Count;
}
```

You always write the member qualified — `OrderStatus.Placed` — so a misspelling is a compile error rather than a
string that silently matches nothing.

### Is it stored as a number or as its name?   {#storage}
By default an enum member is stored as a **number**: `Draft` is 0, `Placed` is 1. That is compact, and it has a sharp
edge — the number means nothing on its own. Anyone reading the table directly, exporting it, or pointing a reporting
tool at it sees `1` and has to come back to the source to learn what that means. Worse, **reordering the members
silently changes what the stored rows mean.**

The **`[Type]`** attribute chooses the storage. `[Type(string)]` stores the member's **name** instead of a number:

```osy title="an enum whose values are readable in the database" test app=enum-declaration
[Type(string)]
enum Priority { Low, Normal, Rush }

entity Ticket {
  [Required] string Title;
  Priority Priority;
}
```

Now the column holds `"Rush"`. It costs a few bytes per row and buys you a table that explains itself, an export that
a human can read, and the freedom to reorder the members without rewriting history.

**Reach for `[Type(string)]` whenever anything outside the app will read the column** — a report, an export, an
integration, a support engineer at 3am. Keep the default numeric form for values that are purely internal.

### How do I loop over every member? — `.Members`   {#members}
`TheEnum.Members` is every member of an enum, in declaration order — the array a picker, a filter bar or a set of
tabs is built from. It reads the type, so it can never drift from it: add a member and every list built this way
grows with it.

**Its element IS the enum value** — `Room.Members` is a `Room[]`, not a list of anything wrapping one. So `r` below
compares with `==` against a `Room` field, passes to anything taking a `Room`, and `r.Label` is a read on the value
itself. `Enum.GetValues<Room>()` is the C# spelling of the same array, and compiles too.

```osy syntax
enum Room { [Label("Living room")] LivingRoom, Kitchen }  // NO [Label] → .Label IS the bare name: "LivingRoom"

// WHERE THE LABEL APPEARS BY ITSELF, and where it does not — the difference is the SLOT, not the value.
Text(kiln.Room);                     // a text slot reads the [Label] label — nothing to write
Badge(kiln.Room.Label, tone: t);     // a `string` PARAMETER takes a string, so say .Label
Text("in " + kiln.Room);             // ⚠ a CONCAT is a string operation: "in LivingRoom", the raw member name.
Row { Text("in "); Text(kiln.Room); }   // …so put the enum in its own text slot when you want the label

// A MEMBER MAY SHARE ITS TYPE'S NAME, exactly as in C#. `Room Room;` is fine and needs no second word.
entity Kiln { Room Room; }

foreach (var r in Room.Members) { Tab(r.Label, selected: r == room, onPress: () => Show(r)); }

Dropdown("Room", value: kiln.Room)            // a dropdown over an enum needs no options at all — see [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/)
```

**The label trap, in one table** — the same value reads two different ways depending on the SLOT it lands in:

| you write | you get | why |
|---|---|---|
| `Text(p.Room)` | `Living room` — the **label** | a text slot RENDERS an enum value, so `[Label]` applies |
| `Text("in " + p.Room)` | `in LivingRoom` — the **member name** | ⚠ `+` is a string op, and an enum's string form is its name (C#-exact). It compiles; it just shows the wrong words |
| `Text("in " + p.Room.Label)` | `in Living room` | `.Label` is the read that hands you the label as a `string` |
| `Badge(p.Room.Label, tone: t)` | `Living room` | a `string` PARAMETER takes a string, so say `.Label` |

`.Label` is a **property, not a method** — `r.Label`, never `r.Label()`. Full story: [[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/).

`.Label` on a member is what a person reads (the `[Label]` text, or the member's own name — see [[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/)).
A generic component can take the same array as a default: `component Picker<T>(Binding<T> value, T[] rows = T.Members)`
— see [generic component](https://osysharp.com/reference/ui/generic-component/).

### How do I add an "All" option to an enum filter?   {#all-filter}
An enum is a closed set, so "no filter" is not one of its members — it is the **absence** of a choice. Hold the
choice in a `T?` and let `null` mean all; the extra tab is then an ordinary tab whose `selected:` asks whether
anything is chosen, and the filter is one `||`:

```osy title="a filter bar with an All option" test app=enum-all-filter
enum Room { Kitchen, Bathroom, Bedroom }

entity Kiln {
  [Required, MaxLength(80)] string Name;
  [Required] Room Where;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

[Page("/kilns")]
[AllowAnonymous]
[Render(CSR)]
[Title("Kilns")]
component Kilns() {
  live var kilns = Kiln.OrderBy(p => p.Name);
  Room? only = null;                                  // null IS "All" — the absence of a choice
  action Show(Room r) { only = r; }
  action ShowAll() { only = null; }

  render {
    Tabs {
      Tab("All", selected: only == null, onPress: ShowAll);
      foreach (var r in Room.Members) { Tab(r.Label, selected: only == r, onPress: () => Show(r)); }
    }
    foreach (var p in kilns) {
      if (only == null || p.Where == only) { Text(p.Name); }
    }
  }
}
```

### The values come from data — should this be an enum?   {#closed}
The set is fixed at compile time. If the values come from data — a list of categories an administrator maintains —
that is not an enum; it is an [entity](https://osysharp.com/reference/entity/declaration/) with rows.

## See also       {#see-also}
- [[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/) — `[Label]`, and what a screen shows for an enum value
- [generic component](https://osysharp.com/reference/ui/generic-component/) — `T[] rows = T.Members`, one component over any enum
- [entity members](https://osysharp.com/reference/entity/properties/) — using an enum as a member type
- [switch](https://osysharp.com/reference/function/switch/) — branching over every case of an enum
- [entity](https://osysharp.com/reference/entity/declaration/) — what to use instead when the values are data, not code


---

<!-- https://osysharp.com/reference/function/cast/ -->

# (int)x — casts

> A C-style cast converts between the numeric types. It truncates toward zero, and it is checked — a value the target type cannot hold fails loudly rather than wrapping to a wrong number.

<!-- id: function-cast · area: function · stability: stable · html: https://osysharp.com/reference/function/cast/ -->

## Summary        {#summary}
A cast converts a value from one numeric type to another: `(int)x`, `(long)x`, `(decimal)x`, `(double)x`. It
truncates toward zero, exactly as C# does, and it is **checked** — a value the target type cannot hold fails with a
message naming the value and the range, rather than silently wrapping.

## Signature      {#signature}
```osy syntax
(int)<value>       // → int      32-bit; truncates toward zero
(long)<value>      // → long     64-bit; truncates toward zero
(decimal)<value>   // → decimal  exact base-10
(double)<value>    // → double   IEEE 754
```

## Description    {#description}

### When do I need a cast?   {#purpose}
Widening happens on its own: an `int` is usable where a `double` or a `decimal` is expected, because nothing is
lost. **Narrowing never happens on its own** — dropping a fraction is a decision, so you write it down. A cast is
how you write it.

The everyday case is a value that is fractional while it is being computed and whole once it is used: a grid cell
from a position, a page number from a ratio, a pixel column from an angle.

```osy title="a fractional value, used as a whole one" test app=function-cast
int CellOf(double position, double cellSize) {
  return (int)(position / cellSize);      // the division is fractional; the cell index is not
}
```

### It truncates toward ZERO   {#truncation}
`(int)2.7` is `2` and `(int)-2.7` is `-2` — toward zero, not toward negative infinity. That matters for any value
that can go negative (a camera coordinate, a delta, a temperature), where truncation and `Math.Floor` disagree:

| value | `(int)v` | `Math.Floor(v)` |
|---|---|---|
| `2.7` | `2` | `2` |
| `-2.7` | `-2` | `-3` |

If you want floor behaviour, say so — `(int)Math.Floor(v)` — and if you want a different rounding, choose it with
[Math.Abs, Math.Sign, Math.Min, Math.Max, Math.Clamp, Math.Truncate, Math.Pow, Math.Sqrt, Math.Sin, Math.Cos, Math.Tan](https://osysharp.com/reference/function/math/) before you narrow. A cast makes no rounding decision for you beyond dropping the fraction.

### What happens when the value does not fit?   {#checked}
C# is *unchecked* by default: `(int)3000000000L` is `-1294967296` there, and `(int)1e20` is formally undefined.
Both are silent wrong answers, so Osy# does not reproduce them. A value outside the target's range **fails**:

```text
cannot cast the value 3000000000 to 'int' — it is outside the range of 'int'
(-2147483648 … 2147483647). An Osy# cast is checked: it fails rather than wrapping to a wrong number.
```

The C# spelling that means the same thing is `checked((int)x)`. `NaN` and `±Infinity` have no integer or decimal
value, so casting one of them to `int`, `long` or `decimal` fails too; both are ordinary values for `(double)`.

The rule holds wherever the expression runs — in a function, in a UI action, in a frame body, and in a query
pushed down to the database.

### A cast does not parse, and it does not format   {#not}
A cast converts **numbers**. It does not parse, and it does not format:

| you wrote | what to write instead |
|---|---|
| `(int)"42"` | `Convert.ToInt(s)` — a parse, which can fail; see [Convert](https://osysharp.com/reference/function/convert/) |
| `(string)total` | `total.ToString()` |
| `(bool)count` | `count != 0` |

Each of those is a compile error naming the alternative. There is no cast to an entity, a `class` or an `enum`
either — casting is the numeric conversion operator, nothing more.

### Casting to `decimal` and `double`   {#widening-casts}
Both directions are legal and both are sometimes what you mean:

- `(decimal)aDouble` takes C#'s conversion — 15 significant digits — so it is the deliberate move from *fast* to
  *exact*. Reach for it at the point money enters the calculation.
- `(double)aDecimal` goes the other way, for geometry and physics, where agreeing with the browser's arithmetic
  matters more than base-10 exactness.
- A cast to the type a value already has (`(double)aDouble`) is legal and does nothing.

```osy title="both directions, on purpose" test app=function-cast
decimal Price(double raw) { return (decimal)raw; }             // fast → exact
double Ratio(decimal part, decimal whole) {
  return (double)part / (double)whole;                          // exact → fast
}
long Micros(decimal amount) { return (long)(amount * 1000000m); }
```

### Does `(int)a * b` cast `a`, or the product?   {#precedence}
A cast binds tighter than arithmetic, exactly as in C#: `(int)a * b` is `((int)a) * b`. Parenthesise the
expression when you mean to convert the whole thing — `(int)(a * b)`.

`(x) - y` is still a subtraction. Only the four type keywords above introduce a cast, so a parenthesised name never
becomes one by accident.

## Examples       {#examples}
```osy title="the whole surface" test app=function-cast
int Truncated() { return (int)-2.7d; }                  // -2 — toward zero
int Floored() { return (int)Math.Floor(-2.7d); }        // -3 — the other rounding, said out loud
long Big(double v) { return (long)v; }
double AsDouble(int n) { return (double)n / 2d; }       // 2.5, not 2 — the cast makes it float division
int Column(double angle, double width) {
  return (int)(angle * width);                          // parenthesised: the product is narrowed
}
```

## See also       {#see-also}
- [Convert](https://osysharp.com/reference/function/convert/) — `Convert.*`, for converting *text* to a number (a parse, which can fail differently)
- [Math.Abs, Math.Sign, Math.Min, Math.Max, Math.Clamp, Math.Truncate, Math.Pow, Math.Sqrt, Math.Sin, Math.Cos, Math.Tan](https://osysharp.com/reference/function/math/) — choose the rounding before you narrow
- [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) — the four numeric types and their literal suffixes
- [long](https://osysharp.com/reference/types/long/) — what a `long` is, and when 32 bits is not enough


---

<!-- https://osysharp.com/reference/function/increment-decrement/ -->

# ++ / -- (increment / decrement)

> Increment or decrement a numeric variable or property by one. In statement position (i++;) and as a for increment the pre- and post-forms are identical. Pre-form in an expression returns the new value; post-form used as a value (old-value semantics) is not supported yet.

<!-- id: function-increment-decrement · area: function · stability: stable · html: https://osysharp.com/reference/function/increment-decrement/ -->

## Summary        {#summary}
`++` and `--` increment or decrement a numeric **lvalue** (a variable or property) by one. In statement
position (`i++;`) and as a loop increment, the pre-form (`++i`) and post-form (`i++`) are identical. As an
expression value the pre-form returns the **new** value, exactly C#; the post-form's old-value semantics
are not supported yet (a pointed compile error, never a silent wrong answer).

## Signature      {#signature}
```osy syntax
++x    // pre-increment  — value is the new x
x++    // post-increment — statement/for only
--x    // pre-decrement
x--    // post-decrement — statement/for only
```

## Description    {#description}
- The operand must be a numeric lvalue — an `int`/`long`/`decimal`/`double` variable or property. A
  non-numeric operand (`s++` on a string) or a non-lvalue (`(a + b)++`) is a compile error.
- Lowered to `x = x ± 1` (the same desugar `+=` uses), so it inherits numeric widening and assignability:
  incrementing a `long`/`decimal`/`double` widens the `1` accordingly.
- **Statement position** (`i++;`) and the **`for` increment** discard the value, so pre- and post-form are
  identical and both exact.
- **Pre-form in an expression** (`var y = ++i;`) returns the new value — exact C#.
- **Post-form used as a value** (`var y = i++;`, `f(i++)`) returns the *old* value in C#, which needs a
  sequencing step the engine doesn't have yet — it's a pointed compile error: *"post-increment 'x++' used
  as a value returns the OLD value — not supported yet; use the pre-form '++x' or put the ++ on its own
  statement."*
- Decompile normalizes to the assignment form (`i = i + 1`), the same round-trip `+=` takes.

## Examples       {#examples}
```osy title="statement + loop + member" test app=increment-examples
entity Gauge { int Hits; }

int Mix() { int i = 0; i++; ++i; i--; return i; }   // 0 +1 +1 -1 = 1

int LoopSum() {
  int total = 0;
  int i = 0;
  while (i < 5) { total = total + i; i++; }          // sum 0..4 = 10
  return total;
}

int PreValue() { int i = 41; return ++i; }           // pre-form returns the new value = 42

int BumpHits() {
  var g = new Gauge { Hits = 10 };
  g.Hits++;                                           // property increment
  g.Hits++;
  return g.Hits;                                      // 12
}
```

## See also       {#see-also}
- [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) — the numeric types `++`/`--` operate on
- [Typed locals](https://osysharp.com/reference/function/typed-locals/) — declaring the counter (`int i = 0;`)


---

<!-- https://osysharp.com/reference/function/collection-literals/ -->

# Array literals

> An array literal [a, b, c] is a value: a list you can return, assign, pass as an argument, or supply as a UI prop. Its element type is inferred from the items. Object literals inside it (new T { … }) are plain data, so a render prop like columns: [ new GridColumn<User> { Name = "Email", Value = u => u.Email } ] is fully expressible.

<!-- id: function-collection-literals · area: function · stability: preview · html: https://osysharp.com/reference/function/collection-literals/ -->

## Summary        {#summary}
An **array literal** `[a, b, c]` is a value expression — a list you can return, assign to a local, pass as an
argument, or hand to a UI control as a prop. The element type is inferred from the items (a homogeneous list is the
common case). Because object literals (`new T { … }`) are plain data, a list of them is a first-class value too — so
a UI prop like `columns: [ new GridColumn<User> { Name = "Email", Value = u => u.Email } ]` is expressed directly,
no workaround.

## Signature      {#signature}
```osy syntax
[1, 2, 3]                                     // int[]
["Email", "DisplayName"]                       // string[]
[ new GridColumn<User> { Name = "Email", Value = u => u.Email } ]   // GridColumn<User>[]
[]                                             // an empty list
```

## Description    {#description}
- **A value, anywhere a value fits** — return it, assign it (`var xs = [1, 2, 3];`), pass it as an argument, or use
  it as a render/state value in a component.
- **Element type is inferred** from the items; a list of `new GridColumn<User> { … }` is a `GridColumn<User>[]`,
  matching a prop declared `GridColumn<T>[]`.
- **Object literals inside are pure data.** In a render position a `new T { … }` builds a plain data object (it is
  *not* a stored row — that is what a `new T { … }` inside an action body does). This is why a control's `columns`
  prop takes `[ new GridColumn<T> { … } ]` cleanly.
- **Distinct from set-membership.** Testing membership is written `[a, b].Contains(x)` (or `x in [a, b]`), which the
  compiler lowers to a set test — separate from using `[a, b]` as a value.

## Examples       {#examples}
```osy title="an array literal as a value" test app=collection-literals
int[] Small() { return [1, 2, 3]; }
string[] Names() { var xs = ["Ada", "Grace"]; return xs; }
```

Supplying a control's typed columns from an array of object literals (a UI prop):

```osy title="object literals as a control's typed columns prop" test app=collection-literals-typed-columns
entity User {
  [Required, MaxLength(200)] string Email;
  [Required, MaxLength(120)] string DisplayName;
  security { allow read when IsAuthenticated; }
}

[Page("/users")] [Render(CSR)]
component UsersPage() {
  var users = User.ToList();
  render {
    DataGrid(
      label: "Users",
      rows: users,
      columns: [
        new GridColumn<User> { Name = "Email", Label = "Email", Value = u => u.Email },
        new GridColumn<User> { Name = "DisplayName", Label = "Name", Value = u => u.DisplayName }
      ]
    );
  }
}
```

## See also       {#see-also}
- [List indexer](https://osysharp.com/reference/function/list-indexer/) — reading an element by index (`xs[0]`)
- [List OrderBy (in-memory)](https://osysharp.com/reference/function/list-orderby/) — sorting a collection
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — foreign controls, whose typed props often take an array literal


---

<!-- https://osysharp.com/reference/function/timespan/ -->

# Building and reading a TimeSpan

> How to build a duration and read it back. Construct one with new TimeSpan(...) or a TimeSpan.FromX factory (including TimeSpan.FromMilliseconds); then read either the WHOLE span in one unit — TimeSpan.TotalHours and friends, fractional — or the individual component in each slot — TimeSpan.Hours and friends, whole. The two are easy to confuse. On a negative span every component is negative.

<!-- id: function-timespan · area: function · stability: stable · html: https://osysharp.com/reference/function/timespan/ -->

## Summary        {#summary}
A `TimeSpan` is a duration ([TimeSpan (durations)](https://osysharp.com/reference/types/timespan/)). You **build** one with `new TimeSpan(...)` — the call the
runtime knows as `TimeSpan.New` — or a `TimeSpan.FromX` factory (`TimeSpan.FromDays`, `FromHours`,
`FromMinutes`, `FromSeconds`, `TimeSpan.FromMilliseconds`). You **read** it two ways that are easy to mix up:
the **totals** (`TimeSpan.TotalDays`, `TotalHours`, `TotalMinutes`, `TotalSeconds`, `TotalMilliseconds`) give
the whole span expressed in one unit, and the **parts** (`TimeSpan.Days`, `Hours`, `Minutes`, `Seconds`) give
the individual component in each slot.

## Signature      {#signature}
```osy syntax
new TimeSpan(<int> hours, <int> minutes, <int> seconds)              -> TimeSpan
new TimeSpan(<int> days, <int> hours, <int> minutes, <int> seconds)  -> TimeSpan
TimeSpan.FromMilliseconds(<number> n) -> TimeSpan     // (and FromDays/FromHours/FromMinutes/FromSeconds)

ts.TotalDays / TotalHours / TotalMinutes / TotalSeconds / TotalMilliseconds  -> double   // the WHOLE span, one unit
ts.Days / Hours / Minutes / Seconds  -> int                                              // the component PARTS
```

## Description    {#description}

### Building a span   {#building}
`new TimeSpan(h, m, s)` and `new TimeSpan(d, h, m, s)` build a span from whole components. The `TimeSpan.FromX`
factories build one from a single, possibly fractional, quantity: `TimeSpan.FromMilliseconds(1500)` is one and
a half seconds, and `TimeSpan.FromDays(1.5)` is a day and a half. (The factories are covered alongside the type
in [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/); `TimeSpan.FromMilliseconds` completes the set here.)

### Totals vs parts — the distinction that trips people up   {#totals-vs-parts}
For the span **1 day, 2 hours, 3 minutes, 4 seconds**:

| Total (whole span, one unit — a `double`) | | Part (the component in that slot — an `int`) | |
|---|---|---|---|
| `TimeSpan.TotalDays` | `1.085462962962963` | `TimeSpan.Days` | `1` |
| `TimeSpan.TotalHours` | `26.051111111111112` | `TimeSpan.Hours` | `2` |
| `TimeSpan.TotalMinutes` | `1563.0666666666666` | `TimeSpan.Minutes` | `3` |
| `TimeSpan.TotalSeconds` | `93784` | `TimeSpan.Seconds` | `4` |
| `TimeSpan.TotalMilliseconds` | `93784000` | | |

`TotalHours` is the **entire** span measured in hours (`26.05…`); `Hours` is just the **hours slot** (`2`).
Totals are fractional (`double`); parts are whole (`int`).

### A negative span has negative components   {#negative}
Subtract a later time from an earlier one and the span is negative. Then **every** component is negative or
zero — `TimeSpan.FromHours(-2)` has `Hours` of `-2` and `Minutes` of `0` (not `60`), and `TotalHours` of `-2`.
Do not assume a component is non-negative.

Every one of these is a pure function of the value, so it runs **in the browser** with no round trip
([execution side](https://osysharp.com/reference/function/execution-side/)).

## Examples       {#examples}
```osy title="how long until a deadline, in whole minutes" test app=timespan
// The WHOLE span in minutes — TotalMinutes, not Minutes (which would drop the hours).
int MinutesUntil(DateTime deadline) {
  var left = deadline - DateTime.UtcNow;
  return Convert.ToInt(left.TotalMinutes);
}
```

```osy title="parts vs totals, and a negative span — pinned" run app=timespan
[Test]
void TimeSpan_parts_and_totals() {
  var span = new TimeSpan(1, 2, 3, 4);         // 1d 2h 3m 4s

  // component PARTS — the piece in each slot, as ints
  Assert.Equal(1, span.Days);
  Assert.Equal(2, span.Hours);                 // the hours SLOT — not the whole span in hours
  Assert.Equal(3, span.Minutes);
  Assert.Equal(4, span.Seconds);

  // a factory, read back through a part
  Assert.Equal(1, TimeSpan.FromMilliseconds(1500).Seconds);   // 1.5s → the seconds slot is 1

  // a NEGATIVE span: every component is negative or zero, never wrapped positive
  var neg = TimeSpan.FromHours(-2);
  Assert.Equal(-2, neg.Hours);
  Assert.Equal(0, neg.Minutes);
}
```

## See also       {#see-also}
- [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/) — the duration type, its `FromX` factories, and storing it on an entity
- [TimeSpan, DateOnly, TimeOnly](https://osysharp.com/reference/types/duration-and-parts/) — `TimeSpan` with `DateOnly` / `TimeOnly`, and `DateTime` arithmetic
- [DateTime](https://osysharp.com/reference/types/datetime/) — subtracting two `DateTime`s to get a `TimeSpan`


---

<!-- https://osysharp.com/reference/function/compound-assignment/ -->

# Compound assignment (+= -= *= /= %= ??=)

> Update a variable or property in place: x op= y is shorthand for x = x op y. Arithmetic forms need a numeric lvalue; ??= assigns only when the left side is null.

<!-- id: function-compound-assignment · area: function · stability: stable · html: https://osysharp.com/reference/function/compound-assignment/ -->

## Summary        {#summary}
Compound assignment updates a variable or property in place: `x op= y` is exactly `x = x op y`. The
arithmetic forms (`+= -= *= /= %=`) require a numeric lvalue; `??=` (null-coalescing assignment) assigns
the right side only when the left is null.

## Signature      {#signature}
```osy syntax
x += y    // x = x + y   (also string concatenation when x is a string)
x -= y    // x = x - y
x *= y    // x = x * y
x /= y    // x = x / y   (int/int truncates, like SQL)
x %= y    // x = x % y   (remainder)
x ??= y   // x = x ?? y  — assign y only when x is null
```

## Description    {#description}
- The left side must be an assignable lvalue — a variable or a property.
- `+= -= *= /= %=` follow the arithmetic rules of their operator: numeric operands, numeric widening
  (`long`/`decimal`/`double`), and int÷int truncation for `/=` (SQL parity). `+=` on a string is
  concatenation.
- `??=` assigns only when the left side is null — a non-null left keeps its value; the right side is
  evaluated only when needed (short-circuit). Value-equivalent to `x = x ?? y` for variable/property
  targets.
- Decompile normalizes to the expanded `x = x op y` form.

## Examples       {#examples}
```osy title="compound assignment" test app=compound-assign
int Mod() { int x = 17; x %= 5; return x; }             // 2

string Keep() { string? s = "have"; s ??= "fallback"; return s; }   // "have" (non-null kept)
string Fill(string? s) { s ??= "fallback"; return s; }              // "fallback" when s is null

decimal RunningTotal(decimal[] amounts) {
  decimal total = 0;
  foreach (var a in amounts) { total += a; }
  return total;
}
```

## See also       {#see-also}
- [++ / -- (increment / decrement)](https://osysharp.com/reference/function/increment-decrement/) — `++`/`--`, the `± 1` special case
- [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) — the numeric types the arithmetic forms operate on


---

<!-- https://osysharp.com/reference/function/datetime-construct/ -->

# Constructing a DateTime, DateOnly, or TimeOnly

> Build a temporal value from its components. DateTime.New takes a date, optionally with a time (defaulting to midnight); DateOnly.New takes a calendar date with no time; TimeOnly.New takes a time of day with no date. For a value coming from a string, Parse is the sibling. All run in the browser.

<!-- id: function-datetime-construct · area: function · stability: stable · html: https://osysharp.com/reference/function/datetime-construct/ -->

## Summary        {#summary}
⚠ **There is no `DateOnly.Today`** (nor a `TimeOnly.Now`) — `DateOnly`/`TimeOnly` have no member that reads the
clock. For today's date as a `DateOnly`, take the date part off the current instant instead:
`DateOnly.FromDateTime(DateTime.UtcNow)` (see [Current time (DateTime.UtcNow, DurableClock.Now)](https://osysharp.com/reference/function/current-time/) for `DateTime.UtcNow`/`DurableClock.UtcNow`).

When you have the components rather than a string, these build the value directly. `DateTime.New(y, m, d)`
makes a date at **midnight**, and `DateTime.New(y, m, d, h, mi, s)` includes the time. `DateOnly.New(y, m, d)`
makes a bare calendar date, and `TimeOnly.New(h, m)` / `TimeOnly.New(h, m, s)` makes a bare time of day.
`DateOnly.FromDateTime(dt)` / `TimeOnly.FromDateTime(dt)` split a `DateTime` into its date or time half — the way to
get "today"/"now" as a bare `DateOnly`/`TimeOnly`, since neither has its own clock read.

## Signature      {#signature}
```osy syntax
DateTime.New(<int> year, <int> month, <int> day)                          -> DateTime  // 00:00:00
DateTime.New(<int> year, <int> month, <int> day, <int> hour, <int> minute, <int> second) -> DateTime
DateOnly.New(<int> year, <int> month, <int> day)                          -> DateOnly
TimeOnly.New(<int> hour, <int> minute)                                    -> TimeOnly   // seconds = 0
TimeOnly.New(<int> hour, <int> minute, <int> second)                      -> TimeOnly
DateOnly.FromDateTime(<DateTime> dt)                                      -> DateOnly   // no DateOnly.Today — this is "today"
TimeOnly.FromDateTime(<DateTime> dt)                                      -> TimeOnly   // no TimeOnly.Now — this is "now"
```

## Description    {#description}
`DateTime.New` builds a `DateTime` from whole components. With three arguments the time is **midnight**; the
six-argument form sets the time explicitly. The result is a UTC instant, consistent with the rest of the date
surface ([Current time (DateTime.UtcNow, DurableClock.Now)](https://osysharp.com/reference/function/current-time/) explains why a `DateTime` here is always UTC).

`DateOnly.New` and `TimeOnly.New` build the two "half" values — a date with no time, and a time with no date
([TimeSpan, DateOnly, TimeOnly](https://osysharp.com/reference/types/duration-and-parts/)). `TimeOnly.New` defaults the seconds to `0` when you pass only hours and
minutes.

**Constructing vs parsing.** Use `New` when you *have* the numeric components; use `DateTime.Parse(s)` (and
`DateOnly.Parse` / `TimeOnly.Parse`) when you have a **string** — for example an ISO timestamp from an API.
The parsing side lives with the current-time surface, [Current time (DateTime.UtcNow, DurableClock.Now)](https://osysharp.com/reference/function/current-time/).

These are pure, so they run **in the browser** with no round trip ([Reading a date — Year, Month, Day, Hour, Minute, Second, DayOfWeek, Date](https://osysharp.com/reference/function/date-parts/) reads the
components back off the value you build).

## Examples       {#examples}
```osy title="the first moment of a given month" test app=datetime-new
DateTime MonthStart(int year, int month) {
  return DateTime.New(year, month, 1);      // day 1, midnight
}
```

```osy title="the exact answers, pinned" run app=datetime-new
[Test]
void Datetime_construct() {
  // three args → midnight
  var midnight = DateTime.New(2024, 3, 15);
  Assert.Equal(15, midnight.Day);
  Assert.Equal(0, midnight.Hour);
  Assert.Equal(1, MonthStart(2024, 3).Day);

  // six args → explicit time
  var full = DateTime.New(2024, 3, 15, 13, 45, 30);
  Assert.Equal(13, full.Hour);
  Assert.Equal(30, full.Second);

  // the half values
  Assert.Equal(29, DateOnly.New(2024, 2, 29).Day);    // a valid leap day
  Assert.Equal(45, TimeOnly.New(13, 45).Minute);
  Assert.Equal(0, TimeOnly.New(13, 45).Second);        // seconds default to 0
  Assert.Equal(30, TimeOnly.New(13, 45, 30).Second);

  // "today" as a bare DateOnly — there is no `DateOnly.Today`; split it off the current instant instead
  var today = DateOnly.FromDateTime(DateTime.UtcNow);
  Assert.Equal(DateTime.UtcNow.Year, today.Year);
}
```

## See also       {#see-also}
- [Current time (DateTime.UtcNow, DurableClock.Now)](https://osysharp.com/reference/function/current-time/) — the current instant, and `DateTime.Parse` for building from a string
- [Reading a date — Year, Month, Day, Hour, Minute, Second, DayOfWeek, Date](https://osysharp.com/reference/function/date-parts/) — reading the components back off a value
- [TimeSpan, DateOnly, TimeOnly](https://osysharp.com/reference/types/duration-and-parts/) — the `DateOnly` and `TimeOnly` types


---

<!-- https://osysharp.com/reference/function/string-search/ -->

# Contains, StartsWith, EndsWith

> Tests whether a string contains, begins with, or ends with another string. The match is case-sensitive and literal — like C#'s String.Contains, the argument is a plain string, not a pattern. The argument may be a computed value. It gives the same answer whether it runs in the browser, in a function body on the server, or pushed down into a database query.

<!-- id: function-string-search · area: function · stability: stable · html: https://osysharp.com/reference/function/string-search/ -->

## Summary        {#summary}
`s.Contains(x)`, `s.StartsWith(x)` and `s.EndsWith(x)` test a string against another string. They behave exactly like
C#'s `String.Contains`/`StartsWith`/`EndsWith`: an ordinal, **literal** match. They work on a local string and inside a
query predicate, and they mean the same thing in both.

## Signature      {#signature}
```osy syntax
<string>.Contains(<string>)   -> bool
<string>.StartsWith(<string>) -> bool
<string>.EndsWith(<string>)   -> bool
```

## Description    {#description}

### The match is literal, not a pattern   {#literal}

The argument is a plain string. A `%` or `_` in it is an ordinary character — `total.Contains("50%")` is true only of a
string that actually contains "50%". This mirrors C# (and EF Core, which escapes these characters when it translates
the call to SQL) — there is no wildcard here.

For a WILDCARD search — `%` for any run of characters, `_` for exactly one — use [Text.Like](https://osysharp.com/reference/function/text-like/), which is a
separate name precisely so the two cannot be confused. It pushes down into a query, and an anchored pattern
(`"RUSH-%"`) can use an index.

For full regular expressions on a string you already hold (a validator, a UI action), use [Regex](https://osysharp.com/reference/stdlib/regex/). Note it
runs **in memory** only: a regex inside a query `.Where(...)` predicate does not compile, because it has no SQL form.
Inside a query the searches that push down are these three literal tests, `Text.Like`, and — for a `[Searchable]`
field — full-text `.Matches(...)`.

### The match is case-sensitive   {#case}

`"ACME Ltd".Contains("acme")` is **false**. If you want a case-insensitive search, lower-case both sides:

```osy title="the match is case-sensitive — lower both sides" syntax
c.Name.ToLower().Contains("acme")
```

### The argument may be computed   {#computed-argument}

It does not have to be a literal written in the source — a variable, a parameter, or any expression that yields a
string works, in every context:

```osy title="the needle may be computed, not just a literal" syntax
name.StartsWith(prefix)   // prefix is a parameter, not a literal
```

### One answer, wherever it runs   {#same-everywhere}

The same expression gives the same result whether it is evaluated on a local string in a browser action, in a function
body on the server, or compiled into SQL and run by the database. That is not a coincidence — it is the property the
three implementations are tested against each other to hold.

## Examples       {#examples}
```osy title="filter orders by a code prefix" test app=string-search
entity Order { string Code; }

List<Order> RushOrders() {
  return Order.Where(o => o.Code.StartsWith("RUSH-")).ToList();
}

bool MatchesTerm(string note, string term) {
  return note.ToLower().Contains(term.ToLower());   // computed argument; lower BOTH sides to ignore case
}
```

## See also       {#see-also}
- [Text.Like](https://osysharp.com/reference/function/text-like/) — the WILDCARD search (`%`, `_`), which does push down into a query
- [Regex](https://osysharp.com/reference/stdlib/regex/) — for full regular expressions on an in-hand string (in memory; not usable in a query predicate)
- [Text.Split](https://osysharp.com/reference/function/text-split/) — the other string builtins
- [execution side](https://osysharp.com/reference/function/execution-side/) — why this runs in the browser too


---

<!-- https://osysharp.com/reference/function/convert/ -->

# Convert

> Explicit conversion between types — number to text, text to number, a Guid to its text form. Osy# will not convert silently where information could be lost, so you say it outright.

<!-- id: function-convert · area: function · stability: stable · html: https://osysharp.com/reference/function/convert/ -->

## Summary        {#summary}
`Convert.*` converts between types explicitly. Osy# does not convert silently where information could be lost — a
`decimal` does not quietly become an `int`, dropping the pence — so where a conversion is what you want, you write it.

## Signature      {#signature}
```osy syntax
Convert.ToInt(<value>)       // → int      (32-bit; truncates toward zero)
Convert.ToInt64(<value>)     // → long     (64-bit; truncates toward zero)
Convert.ToDecimal(<value>)   // → decimal
Convert.ToDouble(<value>)    // → double   (float64)
Convert.ToString(<value>)    // → string   (numbers, Guids, bools …)
Convert.ToBool(<value>)      // → bool

// C#'s own spellings work too — `Convert.ToInt32` IS `Convert.ToInt`, and `Convert.ToBoolean` IS
// `Convert.ToBool`. The same arms, so they behave identically; write whichever you reach for.
Convert.ToInt32(<value>)     // → int
Convert.ToBoolean(<value>)   // → bool
```

## Description    {#description}

### `Convert.*` or a cast?   {#convert-or-cast}
Both narrow, and they are not interchangeable. **Between two NUMBERS, reach for the cast** — `(int)x` — which is the
C# spelling and reads at a glance; see [(int)x — casts](https://osysharp.com/reference/function/cast/). Reach for `Convert.*` when the input might not be a number
at all, or when the output is text:

| | |
|---|---|
| `(int)amount` | a numeric conversion. Truncates toward zero, and **fails** if the value does not fit |
| `Convert.ToInt(s)` | a **parse**. Reads an integer out of text, and answers `0` for text it cannot read |
| `Convert.ToString(v)` | renders a value as text |

The difference in the failure is the reason they are separate: a cast that cannot produce a value stops, while a
parse of user-supplied text answers `0` and carries on — which is right for a form field and wrong for arithmetic.

> ⚠ **This is where Osy# and C# differ, and it is the difference most likely to cost you.** C#'s
> `Convert.ToInt32("abc")` throws `FormatException`. This answers **0** — no error, no null, a number that is
> wrong. So `osy lint` refuses `Convert.To*` over **text** at MUST tier
> (`correctness-parse-that-answers-zero`): say which you mean, and say it where the reader can see it.
>
> ```osy syntax
> if (decimal.TryParse(entered, out var cap)) { … } else { … }   // the C# spelling, and it compiles here
> ```
>
> A **numeric** conversion — `Convert.ToInt(Math.Floor(x))` — is untouched and behaves exactly as C# does. The
> lenient parse is still here and still useful; it just has to be asked for out loud.

### Narrowing is explicit   {#narrowing}
Widening happens on its own — an `int` is usable where a `decimal` is expected, because nothing is lost. Going the
other way loses the fraction, so you must say so:

```osy title="decimal to int, on purpose" test app=function-convert
int WholeUnits(decimal amount) {
  return Convert.ToInt(Math.Floor(amount));   // decide the rounding, THEN narrow
}
```

Note the `Math.Floor` first. `Convert.ToInt` truncates toward zero, which is a rounding decision — and a rounding
decision you make by accident is a rounding bug. Choose it deliberately: `Math.Floor`, `Math.Ceiling`, or
`Math.Round`.

**`Convert.ToInt64`** is the same narrowing to a 64-bit **`long`** instead of a 32-bit `int` — reach for it when the
value can exceed ±2.1 billion (an id, a byte count, a running total of small amounts). It truncates toward zero
exactly as `Convert.ToInt` does, so the same "round on purpose first" rule applies. See [long](https://osysharp.com/reference/types/long/) for what a
`long` is and why its range matters.

```osy title="decimal to long, on purpose" test app=function-convert
long TotalCents(decimal amount) {
  return Convert.ToInt64(Math.Round(amount * 100m));   // money as whole cents, 64-bit headroom
}
```

### How do I get a `double` for non-money maths?   {#to-double}
`Convert.ToDouble` is the float64 front door. Reach for it when the value in hand is a `decimal` and the arithmetic
downstream is not money — geometry, physics, a per-frame camera. `Math.Round`, `Math.Floor`, `Math.Ceiling` and
`Math.Truncate` all answer a **`decimal`** when given one, so this is the conversion back:

```osy title="a rounded decimal, back to float64" test app=function-convert
double SnappedTo(decimal value, decimal step) {
  return Convert.ToDouble(Math.Round(value / step) * step);
}
```

It coerces like its siblings: text is **parsed** (`0` for text it cannot read), `null` is `0`, `true` is `1`. Two
things are true of a `double` that are not true of an `int` or a `decimal`, and both show up here:

| | |
|---|---|
| `Convert.ToDouble("NaN")` · `("Infinity")` · `("-Infinity")` | those are values a `double` HAS, so the text reads (any casing, optional sign) |
| `Convert.ToDouble("1e400")` | `+Infinity` — an out-of-range magnitude is not a parse failure for this type (and `"1e-400"` is `0`) |

⚠ A `double` is **not** money. It cannot hold `0.1` exactly, so converting a price to one and back loses the pence
silently — see [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) for which type a value should have been in the first place.

### What counts as a number   {#grammar}
Every `Convert.To*` reads text the same way, so a string that parses for one parses for all of them. The accepted
shape is:

```text
[white space] [+ or -] digits[,digits…][.digits] [e or E [+ or -] digits] [white space]
```

| | |
|---|---|
| `"1,000"` · `"12,34"` · `"100,"` | group separators are accepted, and their PLACEMENT is not checked |
| `" 42 "` · `"\t42\n"` | a space, tab, newline, vertical tab, form feed or carriage return may pad it |
| `"1e3"` · `"1.5e+2"` · `".5"` · `"1."` | an exponent, a bare fraction and an empty fraction are all fine |
| `",100"` · `"1.0,5"` | ✗ a **leading** separator, or one in the **fraction** |
| `"(5)"` · `"5-"` · `"¤5"` | ✗ accounting parentheses, a **trailing** sign, a currency symbol |
| `" 42"` (non-breaking space) | ✗ a non-breaking space is not white space to a number |
| `"0x1F"` · `"nope"` · `""` | ✗ — and the answer is `0`, never an error and never `NaN` |

The exact same grammar runs in the browser and inside a query the database executes, so a conversion answers the same
thing wherever the expression happens to run. `Convert.ToDouble` reads three more forms, because a `double` has values
the other types do not: `"NaN"`, `"Infinity"` and `"-Infinity"`, in any casing.

An integer conversion additionally refuses a **non-zero fraction** — `Convert.ToInt("42.9")` is `0`, while
`Convert.ToInt("42.0")` is `42`. If you want the number rounded, round it: `Convert.ToInt(Math.Floor(x))`.

### How do I turn a number or a `Guid` into text?   {#to-string}
```osy title="numbers and ids as text" test app=function-convert
string Describe(int count, Guid id) {
  return "count=" + Convert.ToString(count) + " id=" + Convert.ToString(id);
}
```

For building a sentence out of several values, [string interpolation](https://osysharp.com/reference/function/string-interpolation/) reads better —
`$"count={count}"` converts for you.

### Money stays decimal   {#money}
Resist converting money to `int` or back. If you find yourself doing it to make arithmetic work, the accumulator is
probably an `int` that should have been seeded `0m` — see [var](https://osysharp.com/reference/function/var/).

## See also       {#see-also}
- [(int)x — casts](https://osysharp.com/reference/function/cast/) — `(int)x`/`(double)x`, the cast: converting between the NUMERIC types, and failing rather than answering 0
- [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) — the literal suffixes, and which type a bare `0` is
- [String interpolation & format specifiers](https://osysharp.com/reference/function/string-interpolation/) — `$"…{value}…"`, which converts for you
- [Typed locals](https://osysharp.com/reference/function/typed-locals/) — pinning a local's type at its declaration


---

<!-- https://osysharp.com/reference/function/crypto-encrypt/ -->

# Crypto.Encrypt and Crypto.Decrypt

> Encrypt and decrypt values under your app's key, which the platform mints, protects, and rotates for you. There is no key parameter — you never handle key material. Ciphertext is bound to your app, so it is meaningless to anyone else, and encrypting the same value twice gives different ciphertext, so it cannot be used as a lookup key.

<!-- id: function-crypto-encrypt · area: function · stability: stable · html: https://osysharp.com/reference/function/crypto-encrypt/ -->

## Summary        {#summary}
`Crypto.Encrypt(plaintext)` returns an opaque ciphertext string; `Crypto.Decrypt(ciphertext)` returns the original
value. Both run under **your application's own encryption key**, which the platform generates, protects, and rotates.
You never see, choose, store, or pass a key.

## Signature      {#signature}
```osy syntax
Crypto.Encrypt(<string> plaintext)  -> string   // an opaque ciphertext envelope
Crypto.Decrypt(<string> ciphertext) -> string   // the original plaintext
```

## Description    {#description}
Use this for values that must be readable by your app but must not sit in the database in the clear — a stored
third-party credential, an account number, a piece of sensitive personal data. The encryption is AES-256-GCM, which
both conceals the value **and** authenticates it: a ciphertext that has been altered fails to decrypt rather than
quietly returning corrupted data.

### There is no key parameter — on purpose        {#no-key-parameter}
This is the most important thing about this surface. A `Crypto.Encrypt(key, plaintext)` form does not exist, and will
not be added. The moment an app supplies its own key, that key has to live somewhere — and in practice it ends up
committed in source or stored next to the data it protects, which protects nothing. So the platform owns key
management entirely: it mints a key per application, keeps it encrypted at rest, and can rotate it without your app
changing a line.

Two consequences follow, and both are enforced cryptographically rather than by convention:

- **Your ciphertext is yours.** The application is bound into every ciphertext, so a value encrypted by your app
  cannot be decrypted by another — even if the ciphertext leaks, and even though the platform holds every key.
- **Rotation doesn't strand your data.** Each ciphertext records which key wrote it, so values encrypted before a
  rotation keep decrypting afterwards.

### Ciphertext is not a lookup key        {#not-a-lookup-key}
**Encrypting the same value twice gives you two different ciphertexts.** This is required for the encryption to be
sound — reusing the randomness would let an attacker recover the key — but it has a practical consequence worth
stating plainly:

> You cannot find a row by encrypting a value and matching on the result. That query will never match.

If you need to *look up* by a sensitive value, store a **hash** of it alongside the ciphertext and search on the hash
(see [Crypto.Sha256Hex](https://osysharp.com/reference/function/crypto-sha256hex/) — it is deterministic, so it does work in a query). Encrypt what you need to read
back; hash what you need to search by.

`Crypto.Encrypt` and `Crypto.Decrypt` run in memory and are not available inside a query.

## Examples       {#examples}
Store a value encrypted, and search by a hash of it:

```osy title="encrypt what you read back, hash what you search by" test app=crypto-encrypt
entity PaymentMethod {
  string Ciphertext;    // the value itself — recoverable, never stored in the clear
  string Fingerprint;   // a SHA-256 hash — deterministic, so it IS searchable
}

void StoreCard(string accountNumber) {
  new PaymentMethod {
    Ciphertext = Crypto.Encrypt(accountNumber),
    Fingerprint = Crypto.Sha256Hex(accountNumber),
  };
}

// Decrypting is the only way to see the value again.
string RevealCard(PaymentMethod pm) {
  return Crypto.Decrypt(pm.Ciphertext);
}

// Look up by the HASH, never by the ciphertext — encrypting the same number again would
// produce a different envelope, so a ciphertext match would never find anything.
PaymentMethod FindCard(string accountNumber) {
  return PaymentMethod.FirstOrDefault(p => p.Fingerprint == Crypto.Sha256Hex(accountNumber));
}
```

## See also       {#see-also}
- [Crypto.Sha256Hex](https://osysharp.com/reference/function/crypto-sha256hex/) — the deterministic hash to search by (encryption is not searchable)
- [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/) — authenticate a message from outside your app


---

<!-- https://osysharp.com/reference/function/crypto-hmac/ -->

# Crypto.HmacSha256Hex and Crypto.FixedTimeEquals

> HMAC-SHA-256 authenticates a message under a shared key — proving it came from a key holder and was not altered, which a plain hash cannot do. Always verify the resulting tag with Crypto.FixedTimeEquals, never with ==, because ordinary equality leaks how many bytes matched and lets an attacker forge a tag byte by byte.

<!-- id: function-crypto-hmac · area: function · stability: stable · html: https://osysharp.com/reference/function/crypto-hmac/ -->

## Summary        {#summary}
`Crypto.HmacSha256Hex(key, message)` returns the HMAC-SHA-256 tag of `message` under `key` as a **64-character
lowercase hex string**. Unlike a plain hash, it is **keyed**: only someone holding the key can produce a valid tag, so
the tag proves the message came from a key holder and was not altered.

`Crypto.FixedTimeEquals(a, b)` compares two tags **in constant time**. Verify every received tag with it — never with
`==`.

## Signature      {#signature}
```osy syntax
Crypto.HmacSha256Hex(<string> key, <string> message) -> string
Crypto.FixedTimeEquals(<string> a, <string> b) -> bool
```

## Description    {#description}
Use HMAC whenever you must trust a message that arrived from outside: a webhook payload, a signed URL parameter, an
API callback. A plain hash cannot do this job — an attacker who rewrites the payload just recomputes its hash. Because
the HMAC tag depends on a key only you and the sender know, it cannot be recomputed by a third party.

The `key` and the `message` are distinct roles and are **not interchangeable**: swapping them produces a different
(and wrong) tag. The key should come from configuration or the secret store, never a literal in source.

### Verify with FixedTimeEquals, never `==`      {#verify}
This is the part that is easy to get wrong, so it is worth being precise about. String equality **short-circuits**: it
returns as soon as it hits the first differing byte. That means comparing a *wrong* tag that shares a long prefix with
the correct one takes measurably **longer** than one that differs immediately. An attacker who can submit many guesses
and time the responses can exploit that difference to discover the correct tag one byte at a time — and then forge a
valid signature, defeating the whole mechanism.

`Crypto.FixedTimeEquals` compares the full length regardless of where the values differ, so the time it takes reveals
nothing about how close a guess was. A length mismatch simply returns `false` (it does not throw).

The rule is unconditional: **any value being checked against a secret — an HMAC tag, a signature, a token — is
compared with `Crypto.FixedTimeEquals`.**

Both functions run **in memory**. `Crypto.HmacSha256Hex` has no SQL push-down form (a constant-time comparison is
meaningless once a database is doing the matching), so using them in a query predicate is a compile error rather than
a silently weaker check.

## Examples       {#examples}
Verifying a signed webhook — the canonical use, and the canonical mistake it prevents:

```osy title="verify a signed webhook payload" test app=crypto-hmac
// The sender signs the payload with the shared key; we recompute the tag and compare.
// FixedTimeEquals is what makes this safe to expose to an attacker who can retry.
bool IsAuthenticWebhook(string payload, string receivedSignature, string sharedKey) {
  var expected = Crypto.HmacSha256Hex(sharedKey, payload);
  return Crypto.FixedTimeEquals(expected, receivedSignature);
}

// WRONG — never do this. `==` short-circuits on the first differing byte, leaking through
// its timing how much of the tag a guess got right, which lets an attacker forge one:
//   return Crypto.HmacSha256Hex(sharedKey, payload) == receivedSignature;
```

## See also       {#see-also}
- [Signing with raw bytes — Crypto.HmacSha256, Sha256, ToHex, and Text.ToBytes](https://osysharp.com/reference/function/crypto-bytes/) — the byte-in/byte-out HMAC, for a signing CHAIN where each output keys the next call
  (SigV4 and friends). Hex cannot chain: the hex text of a digest is not the digest.
- [Crypto.Sha256Hex](https://osysharp.com/reference/function/crypto-sha256hex/) — the unkeyed secure hash (integrity, not authentication)
- [Crypto.Md5Hex](https://osysharp.com/reference/function/crypto-md5hex/) — the non-adversarial checksum, and why it is not a security primitive


---

<!-- https://osysharp.com/reference/function/crypto-md5hex/ -->

# Crypto.Md5Hex

> MD5 of a string's UTF-8 bytes rendered as 32-character lowercase hex — the canonical C# fingerprint form. Deterministic, so it pushes down into SQL as Postgres md5(). For NON-adversarial content fingerprints and change detection only — MD5 is collision-broken and must never be used for security. Use Crypto.Sha256Hex instead.

<!-- id: function-crypto-md5hex · area: function · stability: stable · html: https://osysharp.com/reference/function/crypto-md5hex/ -->

## Summary        {#summary}
`Crypto.Md5Hex(s)` returns the MD5 digest of `s`'s UTF-8 bytes as a **32-character lowercase hex string** —
the canonical C# `BitConverter.ToString(md5).Replace("-", "").ToLowerInvariant()` form. It is deterministic:
the same input always yields the same digest.

## Signature      {#signature}
```osy syntax
Crypto.Md5Hex(<string> s) -> string
```

## Description    {#description}

### Not a security primitive        {#not-a-security-primitive}
**MD5 is collision-broken.** An attacker can construct two different inputs that produce the same digest — cheaply,
on a laptop. So `Crypto.Md5Hex` must **never** be used to sign, authenticate, verify, or fingerprint anything an
attacker could influence, and never for passwords. If a check answers the question "did this come from someone I
trust?" or "has anyone tampered with this?", MD5 is the wrong tool and using it there is a real vulnerability, not a
style issue.

Reach for these instead:

| If you need to… | Use |
|---|---|
| Hash a value that could be attacker-influenced | [Crypto.Sha256Hex](https://osysharp.com/reference/function/crypto-sha256hex/) |
| Prove a message came from a key holder, unaltered | [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/) |
| Store a password | `Security.HashPassword` (BCrypt — salted and deliberately slow) |

### What it IS for — a non-adversarial content fingerprint                  {#what-it-is-for}
`Crypto.Md5Hex` is kept because it is genuinely the right tool for a **non-adversarial content fingerprint**: hash a
synthesized string and compare it to a stored hash to decide whether downstream work needs to re-run. Nobody is
attacking your cache-invalidation key, and MD5 is cheap and stable.

Because it is deterministic, it **pushes down into SQL**: inside a query it renders as Postgres's `md5()`, which
produces byte-identical lowercase hex, so in-memory and in-database results agree. (`Crypto.Sha256Hex` pushes down too,
so you can use the secure hash in a query without giving that up.)

## Examples       {#examples}
```osy title="fingerprint gate — change detection, not a security check" test app=crypto-md5
// Safe use: deciding whether OUR OWN content changed, so we can skip redundant work.
// Nobody gains anything by forcing a cache miss here.
bool Changed(string content, string storedHash) {
  return Crypto.Md5Hex(content) != storedHash;
}
// Crypto.Md5Hex("hello")  ->  "5d41402abc4b2a76b9719d911017c592"
```

## See also       {#see-also}
- [Crypto.Sha256Hex](https://osysharp.com/reference/function/crypto-sha256hex/) — the secure default hash; use this whenever the input could be attacker-influenced
- [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/) — authenticate a message under a shared key, and verify the tag in constant time
- [Text.Split](https://osysharp.com/reference/function/text-split/) — other in-memory string builtins


---

<!-- https://osysharp.com/reference/function/crypto-sha256hex/ -->

# Crypto.Sha256Hex

> SHA-256 of a string's UTF-8 bytes as 64-character lowercase hex — the secure default hash. Deterministic, so it pushes down into SQL. Use it wherever the input could be attacker-influenced; use Crypto.HmacSha256Hex to authenticate a message, and Security.HashPassword for passwords.

<!-- id: function-crypto-sha256hex · area: function · stability: stable · html: https://osysharp.com/reference/function/crypto-sha256hex/ -->

## Summary        {#summary}
`Crypto.Sha256Hex(s)` returns the SHA-256 digest of `s`'s UTF-8 bytes as a **64-character lowercase hex string** —
the C# `Convert.ToHexString(SHA256.HashData(...)).ToLowerInvariant()` form. It is the **secure default hash**: reach
for this one unless you specifically need a non-adversarial checksum.

## Signature      {#signature}
```osy syntax
Crypto.Sha256Hex(<string> s) -> string
```

## Description    {#description}
SHA-256 is **collision-resistant**: nobody can construct two different inputs with the same digest. That is what makes
it safe to use on values an attacker can influence — which is the property [Crypto.Md5Hex](https://osysharp.com/reference/function/crypto-md5hex/) lacks.

It is deterministic, so the same input always produces the same digest, and it **pushes down into SQL**: inside a query
it renders as Postgres's `sha256()` over the UTF-8 bytes, hex-encoded, producing results byte-identical to the
in-memory version. So a fingerprint you compute in a function matches one you filter on in a query.

**What a hash does and does not prove.** A hash proves **integrity** of a value you already trust — recompute it and
compare to detect corruption or change. It does **not authenticate** a value you received from someone else: an
attacker who can change the value can simply recompute its hash to match. To authenticate a message, you need the
keyed construction — [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/).

**Not for passwords.** A password hash must be deliberately *slow* to resist brute force; SHA-256 is designed to be
fast, which is exactly wrong for credentials. Use `Security.HashPassword` (BCrypt), which also salts for you.

## Examples       {#examples}
Fingerprint content to skip redundant work, and detect tampering in transit:

```osy title="content fingerprint + integrity check" test app=crypto-hashing
class Document {
  public string Body;
  public string Fingerprint;
}

// Skip expensive downstream work when the content is unchanged.
bool NeedsReprocessing(string content, string storedHash) {
  return Crypto.Sha256Hex(content) != storedHash;
}

// Detect corruption of a value we stored ourselves.
bool IsIntact(Document doc) {
  return Crypto.Sha256Hex(doc.Body) == doc.Fingerprint;
}
// Crypto.Sha256Hex("abc") -> "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
```

## See also       {#see-also}
- [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/) — authenticate a message you received (keyed; a plain hash cannot do this)
- [Crypto.Md5Hex](https://osysharp.com/reference/function/crypto-md5hex/) — the fast non-adversarial checksum, and why it must not be used for security


---

<!-- https://osysharp.com/reference/function/current-time/ -->

# Current time (DateTime.UtcNow, DurableClock.Now)

> Read the current instant. `DateTime.UtcNow`/`Today` (and the platform-idiomatic `DurableClock.Now`/…) return the current time from the platform's replay-safe clock — so durable functions resume deterministically and any time-reading logic is time-travel-testable. All are UTC; there is no separate DateTimeOffset type, and `DateTime.Now` names the same instant as `UtcNow`. To read that instant as somebody's wall clock, name their zone.

<!-- id: function-current-time · area: function · stability: stable · html: https://osysharp.com/reference/function/current-time/ -->

## Summary        {#summary}
**`DateTime.UtcNow`** and **`DateTime.Today`** read the current instant, anywhere. Pasted C# that reads "now" runs
unchanged.

Inside a **workflow or durable body**, reach for **`DurableClock.Now` / `.UtcNow` / `.Today`** instead. They read the
instant the ENGINE is processing at — the same one every clock armed on that pass derives from, and the one a pinned
test clock moves. That is the whole difference: `DateTime.UtcNow` in a workflow body answers whatever the machine
says right now, which is neither replay-stable nor testable.

There are three clocks in total, and each is legal exactly where it means something:

| you want | write | where |
|---|---|---|
| the engine's instant, replay-stable | `DurableClock.UtcNow` | a workflow or durable body |
| a one-shot reading of now | `DateTime.UtcNow` | anywhere |
| a value that KEEPS MOVING on screen | `DateTime.UtcNow` | anything that runs on a RENDER PASS — a `render` block, and any method render calls. See [below](#on-screen) |

Reach for the wrong one and the compiler says which to pick: `DurableClock` in a `render` block is refused (it would
freeze at first paint), and outside a durable execution it fails loudly rather than quietly reading wall time.

⚠ **The boundary is WHEN THE CODE RUNS, not which body it is written in.** Pulling a render expression out into a
well-named method does not move it off the render path — it still runs on every pass, so it still wants `DateTime.UtcNow`,
and a `DateTime.UtcNow` left behind there freezes the page exactly as it would have in the slot:

```osy syntax
bool IsDue(Plant p, DateTime now) => p.LastWateredAt.AddDays(p.EveryDays) <= now;  // the instant is passed IN
// ⛔ NOT `bool IsDue(Plant p) => … <= DateTime.UtcNow;` — a clock read INSIDE a helper does not advance, and the
//    compiler refuses it from a `live var`: "Reactivity is decided where the read is WRITTEN, not where the
//    function is called." Read it at the call site — `items.Where(p => IsDue(p, DateTime.UtcNow))`.
action Water(Plant p) { p.LastWateredAt = DateTime.UtcNow; }              // runs once per press → the one-shot read
```

`osy lint` follows the calls, so it flags the first form written with `DateTime.UtcNow` and stays silent on a method
only an action reaches.

⚠ **Every `Now` spelling answers the same instant** — `DateTime.Now`, `DateTime.UtcNow`, `DateTimeOffset.Now`,
`DateTimeOffset.UtcNow`. That is not an alias papering over a difference: this platform has no local-time `DateTime`
for them to differ by. To read the instant as somebody's wall clock, say whose:
`DateTime.UtcNow.InZone(Zone.Of("Europe/Stockholm"))`. See [Time zones — the Zone type and its operations](https://osysharp.com/reference/stdlib/zones/).

### On screen: the clock that keeps moving        {#on-screen}

A reactive context — a `live var` or a render slot — subscribes to the clock, so a value derived from it counts down
on screen with no refetch, no polling and no code beyond the expression:

```osy syntax
live var left = deadline - DateTime.UtcNow;        // ✓ advances
var openedAt  = DateTime.UtcNow;                   // ✓ reads once — the instant the page mounted
```

Every reader on a page ticks **together** — one clock per page, not one per row — so two countdowns never disagree
about what second it is. The cadence is one second; that is a display rate, not a precision claim. A page that never
reads the clock starts no timer and costs nothing.

⚠ **On screen it is a DISPLAY clock, never an authority.** It is the browser's wall clock, so a viewer whose machine
is skewed sees a skewed countdown:

- **Nothing may be decided on it.** Whether an SLA breached, whether an offer expired, whether a token is still
  valid — those are settled on the server against the server's clock and arrive as data. Render that answer.
- The honest division: the server decides **what is true**; the display clock animates **an interval whose endpoint
  is already known**.

## Signature      {#signature}
```osy syntax
DateTime.UtcNow                   // the current instant (UTC)
DateTime.Now                      // the same instant — everything here is UTC
DateTime.Today                    // the current date at midnight (UTC)
DurableClock.Now / .UtcNow / .Today   // inside a workflow/durable body: the ENGINE's instant
DateTimeOffset.Now / .UtcNow      // yields a DateTime (there is no DateTimeOffset type)
```

## Description    {#description}
The platform owns "now". Because a durable function can suspend and resume, a naive system clock would return a
different value on resume and corrupt replay — so every current-time read goes through the platform clock, which
returns the **same instant on replay**. The same mechanism makes time-reading logic **time-travel-testable**: a test
can fix the clock and assert behavior at a chosen moment.

The platform is **UTC-internal**, and that is what makes every `Now` spelling mean one thing. A `DateTime` here *is*
a UTC instant, not a wall-clock-plus-timezone reading, because a server has no single "local" zone to be correct
for — 9am in Frankfurt and 5pm in Tokyo are the same instant, and that instant is what the value holds. So `Now` and
`UtcNow` are not two readings the language collapses into one; there is only ever one reading, and both names say it.
`DateTimeOffset.Now`/`.UtcNow` yield a plain `DateTime` (there is no separate `DateTimeOffset` type), and
`DateTime.Today` is the current date at midnight.

**"Local time" is a question about a person, not about the platform** — which is why it is asked for rather than
assumed. Naming the zone is the whole answer, and it is one line:

```osy syntax
var instant = DateTime.UtcNow;                                  // the fact
var local   = instant.InZone(Zone.Of("Europe/Stockholm"));         // …read as somebody's wall clock
```

Stored and computed `DateTime`s stay UTC; the zone belongs at the display edge, where you know whose it is.

These are current-instant reads, so they run **in memory** (they are not deterministic SQL expressions).

**Parsing and formatting.** `DateTime.Parse(s)` turns a string into a `DateTime` (and `DateOnly.Parse` /
`TimeOnly.Parse` / `TimeSpan.Parse` for the others); each throws on an unparseable string. `value.ToString("fmt")`
formats a date/time/duration with a standard .NET/C# format string (the same engine as a `$"{d:fmt}"` interpolation
hole) — e.g. `d.ToString("yyyy-MM-dd")`. Both are in-memory.

## Examples       {#examples}
```osy title="stamp a row, then ask how old it is" test app=function-current-time
entity Ticket {
  [Required, MaxLength(200)] string Title;
  DateTime DueAt;
}

Ticket Open(string title) {
  return new Ticket { Title = title, DueAt = DateTime.UtcNow + TimeSpan.FromDays(7) };   // faithful C# — the platform clock
}

bool IsOverdue(Ticket t) {
  return DateTime.UtcNow > t.DueAt;
}

bool OpenedRecently(Ticket t) {
  // `CreatedAt` is provided for you — every entity is audited automatically, so you never declare it.
  return DateTime.UtcNow - t.CreatedAt < TimeSpan.FromHours(1);       // TimeSpan arithmetic — see [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/)
}
```

## See also       {#see-also}
- [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/) — durations, and `DateTime`/`TimeSpan` arithmetic


---

<!-- https://osysharp.com/reference/function/date-arithmetic/ -->

# Date arithmetic — AddDays, AddMonths, AddYears, AddHours, AddMinutes

> Move a DateTime forward or back. AddDays/AddHours/AddMinutes take a fractional amount and are exact. AddMonths and AddYears are CALENDAR-aware and CLAMP the day: Jan 31 + 1 month is Feb 29 in a leap year and Feb 28 otherwise, never March 2nd. A negative amount goes backward. They run wherever the expression does — in the browser, on the server, and **inside a query the database executes**, where each lowers to a real SQL interval.

<!-- id: function-date-arithmetic · area: function · stability: stable · html: https://osysharp.com/reference/function/date-arithmetic/ -->

## Summary        {#summary}
These shift a `DateTime` by an amount. `AddDays`, `AddHours` and `AddMinutes` take a **fractional** amount
and are exact time arithmetic. `AddMonths` and `AddYears` are **calendar-aware**: they land on the same
day-of-month where it exists and **clamp** to the last day of the month where it does not. A negative amount
moves backward.

## Signature      {#signature}
```osy syntax
d.AddDays(<number> n)    -> DateTime     // n may be fractional (0.5 = 12 hours)
d.AddHours(<number> n)   -> DateTime
d.AddMinutes(<number> n) -> DateTime
d.AddMonths(<int> n)     -> DateTime     // calendar month, day CLAMPED
d.AddYears(<int> n)      -> DateTime     // calendar year, Feb 29 CLAMPED
```

## Description    {#description}

### They work INSIDE a query — the database does the arithmetic   {#in-a-query}
`AddDays` and its siblings are not client-only helpers. Written inside a `Where`, each lowers to a real SQL
interval, so the comparison happens **in the database** and only the matching rows come back:

```osy syntax
// the whole due-list, computed by the database — one filtered read
live var due = Plant.Where(p => p.LastWateredAt.AddDays(p.WaterEveryDays) <= DateTime.UtcNow).ToList();
```

⚠ **The alternative is a full table read.** `Plant.ToList()` followed by an in-memory `Where` fetches every row on
every page load and filters them in the browser — correct on ten rows, and a table scan on ten thousand. If the
predicate can be written over the entity, write it there: see [Where / Single / Count](https://osysharp.com/reference/query/where/).

### AddDays / AddHours / AddMinutes are exact time arithmetic   {#exact-units}
They add a fixed length of time and roll components over: `AddHours(11)` on `13:45` crosses midnight into the
next day, `AddMinutes(90)` adds an hour and a half. The amount may be **fractional** — `AddDays(0.5)` is
twelve hours, `AddMinutes(0.5)` is thirty seconds — and it may be **negative** to go backward.

### AddMonths / AddYears CLAMP the day — the rule that surprises people   {#clamping}
A calendar month is not a fixed number of days, so "one month after January 31st" has no exact answer.
`AddMonths` and `AddYears` resolve it by **clamping the day to the last valid day of the target month**:

| Call | Result | Why |
|---|---|---|
| `Jan 31 . AddMonths(1)` | `Feb 29` (2024) | Feb has no 31st; clamp to the last day — and 2024 is a leap year |
| `Jan 31 . AddMonths(13)` | `Feb 28` (2025) | thirteen months on, into a non-leap year |
| `Feb 29 . AddYears(1)` | `Feb 28` | the target year has no Feb 29 |

It never spills over into the next month — you will not get "March 2nd" from adding a month to January 31st.
The **time of day is preserved** across all of these.

The member syntax `d.AddMonths(1)` is the everyday spelling; the compiler knows these as `Date.AddDays`,
`Date.AddHours`, `Date.AddMinutes`, `Date.AddMonths` and `Date.AddYears`, and they can also be written in that
call form (`Date.AddMonths(d, 1)`).

Every one is a pure function of the value, so it runs **in the browser** with no round trip
([execution side](https://osysharp.com/reference/function/execution-side/)). To add a duration instead of a fixed unit, add a [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/).

## Examples       {#examples}
```osy title="a due date one month out, honouring month lengths" test app=date-arith
DateTime DueNextMonth(DateTime placed) {
  return placed.AddMonths(1);
}
```

```osy title="fractions, negatives, and month-end clamping — pinned" run app=date-arith
[Test]
void Date_arithmetic() {
  var d = DateTime.New(2024, 3, 15, 13, 45, 30);

  // exact, fractional, and reversible
  Assert.Equal(16, d.AddDays(1).Day);
  Assert.Equal(1, d.AddDays(0.5).Hour);       // +12h → 01:45 next day
  Assert.Equal(16, d.AddHours(11).Day);       // crosses midnight
  Assert.Equal(0, d.AddHours(11).Hour);

  // AddMonths / AddYears clamp the day to the month end
  var jan31 = DateTime.New(2024, 1, 31);
  Assert.Equal(2, jan31.AddMonths(1).Month);
  Assert.Equal(29, jan31.AddMonths(1).Day);   // Feb 29 in a leap year, NOT March 2nd
  Assert.Equal(28, jan31.AddMonths(13).Day);  // Feb 28 in 2025

  var feb29 = DateTime.New(2024, 2, 29);
  Assert.Equal(28, feb29.AddYears(1).Day);    // 2025 has no Feb 29
}
```

## See also       {#see-also}
- [Reading a date — Year, Month, Day, Hour, Minute, Second, DayOfWeek, Date](https://osysharp.com/reference/function/date-parts/) — reading the year/month/day back off the result
- [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/) — adding a *duration* (`d + TimeSpan.FromHours(2)`) rather than a fixed unit
- [Current time (DateTime.UtcNow, DurableClock.Now)](https://osysharp.com/reference/function/current-time/) — the current instant to do arithmetic from


---

<!-- https://osysharp.com/reference/function/enum-words/ -->

# Enum.Label, Enum.Description, Enum.Name

> Read the human-facing words of an enum value in code: its Label (the [Label] text, or the member name when there is none), its Description (the member's doc comment), and its Name (the member's identifier). The screen shows the label for you automatically; reach for these only when a function needs the words.

<!-- id: function-enum-words · area: function · stability: stable · html: https://osysharp.com/reference/function/enum-words/ -->

## Summary        {#summary}
An enum member has three faces you can read in code from a value: `value.Name` is the member's identifier
(`"Placed"`), `value.Label` is what a person reads (the `[Label("…")]` text, or the name when there is
none), and `value.Description` is the member's doc-comment sentence. They lower to the stdlib calls
`Enum.Name`, `Enum.Label` and `Enum.Description`.

## Signature      {#signature}
```osy syntax
value.Name         -> string      // the member identifier, e.g. "Placed"    (Enum.Name)
value.Label        -> string      // the [Label] label, else the name       (Enum.Label)
value.Description  -> string      // the member's /// doc comment              (Enum.Description)
```

## Description    {#description}
These read the words declared with the enum ([[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/)): `[Label("…")]` sets the **label**, a `///`
doc comment sets the **description**, and the member identifier is the **name**.

- **`value.Label`** is the display text. If the member has a `[Label("…")]`, that is the label; otherwise the
  label falls back to the member **name**, because the name is usually already readable.
- **`value.Description`** is the longer sentence from the member's doc comment — the help text under an option,
  for instance. A member with no doc comment has an empty description.
- **`value.Name`** is always the raw identifier, ignoring any `[Label]`.

**You rarely need these for display.** Showing an enum-typed value on a screen already renders its label for
you ([[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/)) — reach for `.Label`/`.Description`/`.Name` when a *function* needs the words: building
a message, an export column, an audit line.

Because the words come from the app's model (which the client already has), these run **in the browser** with
no round trip, just like the rest of the pure surface ([execution side](https://osysharp.com/reference/function/execution-side/)).

## Examples       {#examples}
```osy title="an enum with a label and a description" test app=enum-words
enum OrderStatus {
  /// The order is placed but has not shipped yet.
  [Label("Awaiting shipment")] Placed,

  Shipped,
}

// Build a one-line status message from the words, not the stored value.
string StatusLine(OrderStatus s) {
  return s.Label + " — " + s.Description;
}
```

```osy title="the exact words, pinned" run app=enum-words
[Test]
void Enum_words() {
  // a member WITH a [Label] and a doc comment
  Assert.Equal("Placed", OrderStatus.Placed.Name);                 // the identifier
  Assert.Equal("Awaiting shipment", OrderStatus.Placed.Label);      // the [Label] text
  Assert.Equal("The order is placed but has not shipped yet.", OrderStatus.Placed.Description);

  // a member WITHOUT a [Label]: the label falls back to the name
  Assert.Equal("Shipped", OrderStatus.Shipped.Name);
  Assert.Equal("Shipped", OrderStatus.Shipped.Label);              // no [Label] → the name

  Assert.Equal("Awaiting shipment — The order is placed but has not shipped yet.",
    StatusLine(OrderStatus.Placed));
}
```

## See also       {#see-also}
- [[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/) — declaring the `[Label]` label and the doc-comment description these read
- [enum](https://osysharp.com/reference/enum/declaration/) — the `enum` keyword and how a member is stored
- [switch](https://osysharp.com/reference/function/switch/) — branching on an enum value


---

<!-- https://osysharp.com/reference/function/enumerable-range/ -->

# Enumerable.Range

> A sequence of consecutive integers, count of them, starting at start. It is how you iterate by index — including in a render block, which has foreach and no for.

<!-- id: function-enumerable-range · area: function · stability: stable · html: https://osysharp.com/reference/function/enumerable-range/ -->

## Summary        {#summary}
`Enumerable.Range(start, count)` is a sequence of `count` consecutive integers beginning at `start`. It is what you
`foreach` over when the thing you are iterating is an index rather than a row — and it is the only way to do that
inside a **`render` block**, which has `foreach` and no `for`.

## Signature      {#signature}
```osy syntax
Enumerable.Range(<start>, <count>)   // → int[]  — count integers, starting at start
```

## Description    {#description}

### The second argument is a COUNT, not an end   {#count-not-end}
`Enumerable.Range(0, 4)` is `0, 1, 2, 3` — four values. This is C#'s signature exactly, and the mistake worth naming
once: it is not `Range(first, last)`.

- `count` of `0` is an empty sequence, and iterating it renders nothing. That is the case a count computed from data
  hits first, so it is well-defined rather than an error.
- A negative `count`, or a range whose last value would pass `int.MaxValue`, fails loudly.

### A fixed grid in a render block   {#in-render}
A `render` block iterates with `foreach`; there is no `for`. So anything laid out by position — a board, a calendar
month, a star rating, a fixed number of skeleton placeholders — is a `foreach` over a range:

```osy title="an 8x8 board" test app=function-range
[Page("/board")]
[AllowAnonymous]
component Board() {
  render {
    Stack(gap: 0) {
      foreach (var row in Enumerable.Range(0, 8)) {
        Row(gap: 0) {
          foreach (var col in Enumerable.Range(0, 8)) {
            Box(w: "40px", h: "40px", bg: (row + col) % 2 == 0 ? "#eee" : "#333");
          }
        }
      }
    }
  }
}
```

The inner range is evaluated per outer element, exactly as a nested loop reads.

### A range or a `for` loop, in a function body?   {#in-a-body}
It works in a function too, and there it is a matter of taste against [for](https://osysharp.com/reference/function/for-loop/): reach for `for` when you
need the index to *drive* something (a mutable step, an early exit), and for a range when you are iterating a fixed
number of values.

```osy title="a range in a body" test app=function-range
int SumTo(int n) {
  int total = 0;
  foreach (var i in Enumerable.Range(1, n)) { total = total + i; }
  return total;
}
```

### Is it lazy? — no, it MATERIALISES   {#materialises}
C#'s `Enumerable.Range` is lazy; this one produces the whole sequence. So it is for a grid, not for a per-pixel loop:
a few hundred or a few thousand values is ordinary, and a range of hundreds of thousands allocates all of them. The
sequence is a real list — `foreach` it, take its `.Count`, or run the LINQ surface over it.

## Examples       {#examples}
```osy title="counting, and the empty case" test app=function-range
int Count() { return Enumerable.Range(0, 5).Count; }          // 5
int Empty() { return Enumerable.Range(0, 0).Count; }           // 0 — well-defined, not an error
int Offset() { return Enumerable.Range(3, 4).Count; }          // 3, 4, 5, 6
int EvensTo(int n) { return Enumerable.Range(0, n).Where(i => i % 2 == 0).Count; }
```

## See also       {#see-also}
- [foreach](https://osysharp.com/reference/function/foreach/) — the loop this feeds
- [for](https://osysharp.com/reference/function/for-loop/) — the indexed loop a function body can use instead (a render block cannot)
- [[ui-component#render-tree]] — what a render block may contain
- [Array literals](https://osysharp.com/reference/function/collection-literals/) — a sequence written out (`[1, 2, 3]`) rather than generated


---

<!-- https://osysharp.com/reference/function/index/ -->

# Functions (the unit of work)

> A function is where your app's logic lives — a top-level unit of work, written like a C# method, that runs on the server. It is transactional by return: no Save(), no UnitOfWork.Commit(). It can call out to the world with no async and no Task, because the engine suspends and resumes it durably. And it contains no authorization code, because the entity's rules do that job — which is why a function is usually just the business problem, and nothing else.

<!-- id: function-index · area: function · stability: stable · html: https://osysharp.com/reference/function/index/ -->

## Summary        {#summary}
A function is the unit of work. It looks like a C# method, it lives at the top level of a file, and it runs on the
**server**.

Four things are true of every one, and together they are the whole model:

1. **It is transactional by return.** What it writes commits when it finishes. There is no `Save()` and no `UnitOfWork.Commit()`.
2. **It has no colour.** It can call out to the world — HTTP, a model, a file — with no `async`, no `Task<T>`, and no
   change to its signature or to anyone who calls it.
3. **It contains no authorization code.** It runs as the caller, and the entity's `security { }` rules decide what it
   is allowed to touch.
4. **A fault undoes it.** If it throws, the rows it wrote are discarded — there is no half-done state to clean up.

```osy title="a whole unit of work" test app=function-index
entity Order {
  [Required, Unique, MaxLength(20)] string Code;
  decimal Total;
  invariant Total >= 0;

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }    // the rules live HERE — never in the function
}

Order Place(string code, decimal total) {
  var order = new Order { Code = code, Total = total };
  return order;
}                       // ← committed here. No Save(), no auth check, no try/catch, no DTO.
```

Read what is *absent*: no repository, no transaction scope, no `IsAuthorized` call, no mapping to a response type.
That absence is the point — [the execution model](https://osysharp.com/reference/project/index/) explains why none of it exists.

## Description    {#description}

### It is a transaction, and the boundary is `return`   {#transactional}
Everything a function writes lands **together, or not at all**:

```osy title="two rows, one outcome" test app=function-index
entity AuditLine {
  [Required, MaxLength(200)] string Message;
  security { allow create, read when IsAuthenticated || IsAnonymous; }
}

void PlaceAndLog(string code, decimal total) {
  var order = new Order { Code = code, Total = total };
  var line  = new AuditLine { Message = $"placed {code}" };
  // there is no state in which the order exists and the log line does not
}
```

You never write change tracking. The platform knows which rows you created and which fields you touched — no dirty
flags, no diffing, no save pipeline. **On the server you do not even choose when to persist**; returning is the
commit.

This is the one place the client differs, and it is worth knowing before it surprises you: in a **UI action**, the
same `new Order { … }` is *staged* and shows on screen immediately, and it persists when a `UnitOfWork.Commit()` runs. Same
statement, two moments — the asymmetry, and the reason for it, is in
[the execution model](https://osysharp.com/reference/project/index/) (§when data persists). Do not reach for `UnitOfWork.Commit()` in a server function.

### The durable model — why there is no `async`   {#durable}
**This is the paragraph to understand.** When a function reaches something that leaves the process — an HTTP call, a
model completion, a file read — the engine **suspends** it, performs the effect, and **resumes it at the next line**,
with every local still in place.

That suspension is *durable*. If the process is restarted, redeployed or killed while the call is in flight, the
function still resumes where it left off. It is not a thread parked in memory; it is a continuation the platform
persisted.

So you write this:

```osy title="calling out to the world, with no ceremony" test app=function-index-http
app Shop {
  model "model/**/*.osy";
  use Osysharp.Http;
}

string Fetch(string url) {
  var response = Http.Get(url);      // the function pauses here — you did not have to say so
  return response.IsSuccess ? response.Body : "";
}
```

No `async`. No `Task<string>`. Nothing about the signature says it might take a while, and **no caller has to change**
when you add an outward call three layers down.

In C#, `async` is a *colour*: a method that awaits must be `async`, so its callers must await it, so they must be
`async` too — it spreads until it reaches `Main`. The colour exists so a caller knows the callee might yield. Here
that is the engine's business rather than the signature's: **any** function can suspend, so none has to advertise it.
There is nothing to spread, so there is nothing to mark. The full story, including the single place `await` does
appear, is in [async / await — why Osy# has neither](https://osysharp.com/reference/function/async-await/) — if you are coming from C#, it is the first habit to unlearn.

### What crosses the wire, and what does not   {#effects}
The engine hands off to the server when — and only when — a statement genuinely needs the server. It is worth knowing
which those are, because the syntax hides them:

| These reach the server | These do not |
|---|---|
| a **query** or any read of stored data (`Order.Single(…)`) | **pure computation** — arithmetic, string work, comparisons |
| an **effect** — `Http.*`, `File.*`, `LlmClient.*`, `Log.*`, `Memory.Search` | **control flow** — `if`, `foreach`, `while`, `switch` |
| a **call to another function** | **`new T { … }` and assignment** — they accumulate in the unit of work |
| `UnitOfWork.Commit()` / `cancel()` | reading fields of rows you already have |
| raising or starting a **workflow** | building and looping over a local `List<T>` |

The right-hand column is the surprising one: **creating a row and assigning to it do not force a round trip.** They
accumulate in the open unit of work and travel with it. So a loop that builds fifty rows is one unit of work, not
fifty conversations.

The thing actually worth noticing is a loop that calls **another function** ten times — that is ten hand-offs. The
syntax hides the round trip; the latency does not.

### Security is ambient — a function has no auth code   {#security}
A function runs **under the caller's security context**, and the entity's [`security { }`](https://osysharp.com/reference/security/entity-security/)
rules do the authorization. You do not check permissions in a function, and you should not try to.

This is not a convenience. A check you write is a check someone can forget to write; a rule on the entity is enforced
for **every** path that touches it — this function, the next one, the UI, a workflow, an imported CSV — with no way
to route around it.

Two consequences follow, and both catch people out.

**`user` does not exist inside a function body.** It is an ambient of the *security rules*, not of your code — writing
`user.Id` in a function is an unknown-identifier error. There is deliberately no "current user" to read: authorization
is a property of the data, expressed once on the entity, not a value you fetch and branch on.

**The caller's identity still reaches the row anyway** — through the audit columns. `CreatedBy` is stamped from the
security principal, which is the same thing `user.Id` resolves to inside a rule. That is what makes ownership work
with **no owner field and no assignment anywhere in your code**:

```osy title="ownership, with nothing in the function to assign it" test app=function-index-owner
[Role] enum AppRole { Authenticator, Member }

[Principal]
entity User {
  [Required, MaxLength(255)] string Email;
  security { allow read where Id == user.Id; }
}

entity Note {
  [Required, MaxLength(200)] string Title;

  // `CreatedBy` is stamped for you, from the signed-in principal.
  security {
    allow create when IsAuthenticated;
    allow read, update, delete where CreatedBy == user.Id;   // …so this is ownership, for free
  }
}

void Write(string title) {
  var note = new Note { Title = title };   // nobody sets an owner. There is no owner field.
}
```

Every reader of `Note` now sees only their own — including `Note.Count()`, which honestly means *"how many notes are
mine"*.

And the enforcement is real with **nothing in the function at all**. `Write` contains not one line of security code,
so the only thing that can refuse it is the entity's rule — and it does:

```osy title="zero authorization code — and it is still enforced" run app=function-index-owner
[Test]
void The_function_has_no_auth_check_and_is_still_gated() {
  // Nobody is signed in. Note grants create only `when IsAuthenticated`, that rule is part of every
  // write, and so the create is refused — without `Write` knowing that security exists.
  Assert.Denied(() => Write("a note"));

  Assert.Empty(Note.ToList());
}
```

See [the security guide](https://osysharp.com/reference/security/index/) for the whole model.

### Errors   {#errors}
A fault — one you `throw`, a broken [invariant](https://osysharp.com/reference/entity/invariants/), a violated [constraint](https://osysharp.com/reference/entity/constraints/) —
**discards everything the function wrote** and travels to the caller. There is no half-applied state, and nothing to
unwind by hand.

```osy title="the first row does not survive the second's refusal" test app=function-index
void PlaceTwo(string first, string second) {
  var a = new Order { Code = first, Total = 10m };

  if (second == "") { throw new ValidationException("the second code is required"); }

  var b = new Order { Code = second, Total = 20m };
}
```

You can also **handle** one. Constraint and invariant violations arrive as an ordinary `ValidationException`, so a
caller that wants to answer for a bad row rather than fail on it just catches it — and a `try` block that throws
discards what *it* wrote, so the handler is never left holding a broken half-row:

- [throw](https://osysharp.com/reference/function/throw/) — raising a fault, and the closed set of five types
- [try / catch / finally](https://osysharp.com/reference/function/try-catch/) — catching one, `when` filters, `finally`, and the per-block rollback

### What a signature may say   {#signatures}
Written like C#, with the deviations worth knowing:

```osy title="parameters, defaults, and named arguments" test app=function-index
decimal Quote(decimal amount, decimal rate = 0.25m, string? note = null) {
  return amount * (1m + rate);
}

decimal Two() {
  var a = Quote(100m);                      // rate defaults
  var b = Quote(100m, rate: 0.1m);          // named argument
  return a + b;
}
```

- **Return** `void`, a scalar, an enum, an entity, a `class`, or a `List<T>` / `T[]`. Every path must return
  ([function](https://osysharp.com/reference/function/declaration/)); a `throw` counts as a path.
- **Default parameter values** and **named arguments** work as in C#. A **nullable** parameter (`string? note`) is
  optional even without a default — omit it and it binds `null`.
- **Overloads do not exist.** Two functions may not share a name; give the second one a name that says what it does.
- **Recursion works**, and is capped — a runaway function is stopped by the platform, and that stop cannot be caught
  ([try / catch / finally](https://osysharp.com/reference/function/try-catch/)).
- Names are **PascalCase**; parameters are **camelCase**.

### Who calls a function   {#callers}
The same function, unchanged, is reachable from all of these — it does not know or care which one it is serving:

- **A UI action** — `Publish(draft);` by name. The engine crosses the boundary ([How an Osy# app works (the execution model)](https://osysharp.com/reference/project/index/)).
- **Another function** — an ordinary call.
- **A workflow** — as a state's work.
- **A test** — `[Test]` calls it directly, with real data and real rules ([[Test] / [TestFixture]](https://osysharp.com/reference/testing/test/)).
- **The outside world** — *if* you publish it. A function is exposed over REST or as a tool by declaring it in the app
  manifest; you never write an endpoint, and the function stays transport-agnostic. That surface is for *other
  people's* integrations, never for your own UI.

### Where a function lives   {#where}
At the **top level of a file**, beside the data — not inside the entity. An entity body holds data and its rules;
behaviour sits next to it. If you want behaviour *attached* to a type, with a receiver, that is a
[class method](https://osysharp.com/reference/class/methods/).

## The pages       {#the-pages}
Everything a function body may contain.

**Declaring one**
- [function](https://osysharp.com/reference/function/declaration/) — the shape, the return, the transaction
- [async / await — why Osy# has neither](https://osysharp.com/reference/function/async-await/) — why there is no `async`, and the one place `await` appears
- [class methods](https://osysharp.com/reference/class/methods/) — behaviour attached to a type instead

**Control flow**
- [if / else](https://osysharp.com/reference/function/if/) · [switch](https://osysharp.com/reference/function/switch/) — branching
- [foreach](https://osysharp.com/reference/function/foreach/) · [for](https://osysharp.com/reference/function/for-loop/) · [while](https://osysharp.com/reference/function/while-loop/) — looping
- [break / continue](https://osysharp.com/reference/function/break-continue/) — leaving a loop early

**Errors**
- [throw](https://osysharp.com/reference/function/throw/) — raising a fault
- [try / catch / finally](https://osysharp.com/reference/function/try-catch/) — handling one

**Values and locals**
- [var](https://osysharp.com/reference/function/var/) · [Typed locals](https://osysharp.com/reference/function/typed-locals/) · [const](https://osysharp.com/reference/function/const/) — declaring locals
- [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) — numeric types and literal suffixes
- [String interpolation & format specifiers](https://osysharp.com/reference/function/string-interpolation/) — `$"…"` and format specifiers
- [Array literals](https://osysharp.com/reference/function/collection-literals/) · [List indexer](https://osysharp.com/reference/function/list-indexer/) · [List OrderBy (in-memory)](https://osysharp.com/reference/function/list-orderby/) — lists
- [Sum / Average / Min / Max / Count](https://osysharp.com/reference/query/aggregates/) · [LINQ over a local list](https://osysharp.com/reference/query/in-memory-linq/) — `Sum`/`Average`/`Min`/`Max`/`Count` and the rest of LINQ,
  over a plain `List<T>` you are holding as much as over a query. There is no accumulator loop to write.
- [Compound assignment (+= -= *= /= %= ??=)](https://osysharp.com/reference/function/compound-assignment/) · [++ / -- (increment / decrement)](https://osysharp.com/reference/function/increment-decrement/) — `+=`, `++`
- [(int)x — casts](https://osysharp.com/reference/function/cast/) — `(int)x`, narrowing between the numeric types
- [Enumerable.Range](https://osysharp.com/reference/function/enumerable-range/) — `Enumerable.Range(0, 8)`, a sequence of integers to iterate
- [Convert](https://osysharp.com/reference/function/convert/) — converting between types
- [nameof](https://osysharp.com/reference/function/nameof/) — a member's name as a string

**The standard library, from a function body**
- [Current time (DateTime.UtcNow, DurableClock.Now)](https://osysharp.com/reference/function/current-time/) — `DateTime.UtcNow`, `DurableClock.Now`
- [Guid.Empty and Guid.NewGuid](https://osysharp.com/reference/function/guid-statics/) — `Guid.NewGuid()`, `Guid.Empty`
- [Text.Split](https://osysharp.com/reference/function/text-split/) · [Text.LastIndexOf](https://osysharp.com/reference/function/text-lastindexof/) — text
- [Crypto.Sha256Hex](https://osysharp.com/reference/function/crypto-sha256hex/) · [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/) · [Signing with raw bytes — Crypto.HmacSha256, Sha256, ToHex, and Text.ToBytes](https://osysharp.com/reference/function/crypto-bytes/) · [Crypto.Encrypt and Crypto.Decrypt](https://osysharp.com/reference/function/crypto-encrypt/) · [Crypto.Md5Hex](https://osysharp.com/reference/function/crypto-md5hex/) — hashing, signing, encryption
- [reading a secret's value (Secret.Name)](https://osysharp.com/reference/function/secret-read/) — `Secret.Name`, a declared secret's value in a body
- [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/) — password hashing and token issuing
- [Log.*](https://osysharp.com/reference/diagnostics/log/) — `Log.Information(…)` and friends
- [Http.*](https://osysharp.com/reference/http/facade/) — calling someone else's API

## See also       {#see-also}
- [How an Osy# app works (the execution model)](https://osysharp.com/reference/project/index/) — the execution model: what runs where, and when data persists
- [Querying data](https://osysharp.com/reference/query/index/) — reading data, and why a security rule is part of the query
- [The security model](https://osysharp.com/reference/security/index/) — the rules that a function deliberately does not contain
- [[Test] / [TestFixture]](https://osysharp.com/reference/testing/test/) — proving a function does what you think, against real data and real rules
- [function](https://osysharp.com/reference/function/declaration/) — the concept page for the declaration itself


---

<!-- https://osysharp.com/reference/function/guid-statics/ -->

# Guid.Empty and Guid.NewGuid

> The C# Guid statics. `Guid.Empty` is the all-zero Guid constant, spelled without parens; `Guid.NewGuid()` mints a fresh unique Guid. Guid.Empty pushes down into SQL predicates; Guid.NewGuid() is in-memory only.

<!-- id: function-guid-statics · area: function · stability: stable · html: https://osysharp.com/reference/function/guid-statics/ -->

## Summary        {#summary}
`Guid.Empty` is the all-zero Guid (`00000000-0000-0000-0000-000000000000`) — a constant, spelled **without
parens** exactly like C#'s static property. `Guid.NewGuid()` mints a fresh, unique Guid on each call. Both
are typed `Guid`.

## Signature      {#signature}
```osy syntax
Guid.Empty        // the all-zero constant (a property — no parens)
Guid.NewGuid()    // a fresh, unique Guid (a factory call)
```

## Description    {#description}
`Guid.Empty` is a deterministic constant. It is the idiomatic sentinel for an unset Guid — e.g. guarding a
reference before use:

```osy syntax
if (ownerId != Guid.Empty) { … }
```

Because it is a constant, `Guid.Empty` **pushes down into SQL** — it is usable inside a query predicate
(`Widget.Where(w => w.Id != Guid.Empty)`), where it renders as the zero-uuid literal.

`Guid.NewGuid()` is **non-deterministic** (a new value every call), so — like the crypto/random generators
— it runs **in memory only** and has no SQL push-down form; calling it inside a query predicate is an error.

`Guid.NewGuid()` is the faithful C# spelling and the single way to mint a Guid (it replaced the earlier
`Security.NewGuid()`).

### `Guid.Empty` or `Guid.NewGuid()`? — the parentheses are enforced   {#parens-enforced}

Swapping the two spellings is an error, in both directions, exactly as it is in C#:

```osy syntax
Guid.Empty()      // error — `Guid.Empty` is a property, not a method
Guid.NewGuid      // error — `Guid.NewGuid` is a method; call it with parentheses
```

The distinction is not decoration: `Guid.Empty` is a value that is always the same one, and `Guid.NewGuid()`
mints a new value every time it runs. The parens are how a reader tells those apart at a glance, so the
compiler holds you to them. The same rule covers every parenless member of the standard library —
`TimeSpan.Zero`, `DateTime.UtcNow`, `DateTime.Today`, `DateTimeOffset.UtcNow` and the `DurableClock` reads.

## Examples       {#examples}
```osy title="sentinel guard + minting" test app=guid-statics
Guid PickOwner(Guid requested) {
  if (requested != Guid.Empty) {
    return requested;          // caller supplied one
  }
  return Guid.NewGuid();       // otherwise mint a fresh owner id
}
```

## See also       {#see-also}
- [String interpolation & format specifiers](https://osysharp.com/reference/function/string-interpolation/) — Guids stringify through the same value coercion


---

<!-- https://osysharp.com/reference/function/deep-object-graphs/ -->

# How deep can an object graph get?

> A value built out of references to other values — a linked list, a tree, a chain of parents — may nest up to 500 levels. Past that, serializing it across a suspend or through JsonSerializer.Serialize is refused with an error naming the depth and the class. A cycle is fine across a suspend and refused by JsonSerializer.

<!-- id: function-deep-object-graphs · area: function · stability: stable · html: https://osysharp.com/reference/function/deep-object-graphs/ -->

## Summary        {#summary}
A `class` whose field is another instance of the same class lets you build a **chain**, a **tree**, or any other
graph of references, as deep as your data goes. Two things later have to turn that graph into one document — parking
it across an `await`, and [JsonSerializer](https://osysharp.com/reference/json/serializer/) — and both cap it at **500 levels of nesting**.

Under 500, nothing about this is visible. Past it you get an error naming the depth, the limit, and the class the
walk was inside when it stopped.

## Signature      {#signature}
```osy syntax
// 500 levels of nesting, counting the value itself as level 1
class Node { public int V; public Node? Next; }   // Next → Next → Next → … up to 500
```

## Description    {#description}

### Which values does this cover?   {#which-values}
Any value whose nesting your DATA decides rather than your source: a `class` field pointing at another instance, a
`List` of `List`s, a `Map` whose values are maps. A field on an `entity` never counts — a relation is a FK, not a
nested value, and serializing an entity is shallow by design ([JsonSerializer](https://osysharp.com/reference/json/serializer/)).

Depth is counted the way you would read it: the value itself is level 1, the thing its field points at is level 2.
A 500-link chain plus its terminating `null` is 501 levels, so the last link that fits carries 499 before it.

### Where does the limit apply?   {#where}
Two places, both of which turn a live graph into one stored document:

| Where | What happens past 500 |
|---|---|
| A value held across an `await` that suspends (a workflow parking, a client hand-off, a durable step result) | the suspend fails with `a value held across a suspend is nested N levels deep — the limit is 500 (reached inside class 'X')` |
| `JsonSerializer.Serialize(value)` | `the value passed to JsonSerializer.Serialize is nested N levels deep — the limit is 500 (reached inside class 'X')` |

Both are ordinary errors: they are raised where the serialization happens, they name the class, and code around them
can catch them. Building the graph is never refused — only storing one that deep.

### Why 500, and not "as deep as you like"?   {#why}
A parked continuation is persisted as **one JSON document**, and the reader that has to parse it back is bounded by
the machine's stack whatever this platform does. 500 sits far enough below that ceiling to be safe on every thread
the runtime uses, and far enough above any shape real data takes to be invisible.

If you are near it, the graph is almost certainly the wrong thing to be holding across the wait: park an **id**, or
the handful of fields the code after the `await` actually reads, and re-derive the rest when it resumes. That is
cheaper as well as shorter — the whole graph is written, stored and read back on every suspend.

### What about a graph that points back at itself?   {#cycles}
The two surfaces answer differently, and both answers are deliberate.

**Across an `await`, a cycle is fine.** The durable form records object identity, so a value reachable twice is
written once and referred to afterwards. A node whose `Next` is itself parks and resumes as a node whose `Next` is
itself — one object, not a copy — and a change made through one path is seen through the other.

**Through `JsonSerializer.Serialize`, a cycle is refused**, with its own message rather than a depth one:

```text
JsonSerializer.Serialize cannot serialize an instance of class 'Node', because it refers back to itself — JSON has
no way to write a reference to a value it has already written. Break the cycle before serializing (drop the
back-pointer, or serialize the id instead of the object).
```

JSON has no spelling for a back-reference, so there is nothing faithful to write. The same value appearing twice in
different branches is **not** a cycle and is written out twice, as you would expect.

## Examples       {#examples}

```osy title="a chain built in a loop — the shape that reaches the limit" test app=function-deep-object-graphs
class Node { public int V; public Node? Next; }

// Nothing here is checked at compile time: the expression is trivial and the depth is `n`, a runtime value.
Node? Build(int n) {
  Node? head = null;
  for (int i = 0; i < n; i++) { head = new Node { V = i, Next = head }; }
  return head;
}

string AsJson(int n) {
  return JsonSerializer.Serialize(Build(n));   // fine while n < 500; a named error past it
}
```

```osy title="hold the id across the wait, not the graph" test app=function-deep-object-graphs
entity Batch { [Required, MaxLength(50)] string Code; int Size; }

class Crumb { public string Code; public int Size; }

// The few fields the code after the wait actually reads — flat, tiny, and immune to how deep the data got.
Crumb Summarize(Batch b) {
  return new Crumb { Code = b.Code, Size = b.Size };
}
```

## Notes          {#notes}
- The limit is on NESTING, not on size. A list of a million flat items is depth 2 and stores fine; a chain of 501 is
  not, however small each link is.
- Nothing about this is visible while you build the graph. It is a property of storing one, so the error appears at
  the `await` or the `Serialize` call, not where the data was assembled.

## See also       {#see-also}
- [JsonSerializer](https://osysharp.com/reference/json/serializer/) — the serialize surface this bounds, and what it does with each value shape
- [constructor](https://osysharp.com/reference/class/constructors/) — the `class` types whose fields make a graph nestable in the first place
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — the durable path a parked value has to survive


---

<!-- https://osysharp.com/reference/function/list-orderby/ -->

# List OrderBy (in-memory)

> Sort a local List<T> in memory by a key selector, returning a NEW sorted List<T> (the source is untouched). Ascending or descending, stable, exactly like C#'s Enumerable.OrderBy; chain Take(n) for top-k. Distinct from an entity-query OrderBy, which lowers to SQL.

<!-- id: function-list-orderby · area: function · stability: stable · html: https://osysharp.com/reference/function/list-orderby/ -->

## Summary        {#summary}
`list.OrderBy(x => x.Key)` sorts a local `List<T>` by the selected key and returns a **new** sorted
`List<T>` — the source list is left unchanged, exactly like C#'s `Enumerable.OrderBy(...)`.
`OrderByDescending` sorts in reverse. The sort is **stable**: elements with equal keys keep their input
order.

## Signature      {#signature}
```osy syntax
list.OrderBy(<element> x => <key>)            // ascending  → a new List<T>
list.OrderByDescending(<element> x => <key>)  // descending → a new List<T>
```

## Description    {#description}
The receiver is a `List<T>` (`new List<T>()` or a `Text.Split` result). The key selector `x => x.Key`
projects each element to a **comparable** value (a number, string, date, …); a non-comparable key is a
compile error. The result is itself a `List<T>`, so it can be iterated, indexed
(`sorted[0]`, see [List indexer](https://osysharp.com/reference/function/list-indexer/)), counted, and sorted again.

This is an **in-memory** sort with no database dependency — the twin of an entity-query `OrderBy`, which
instead lowers to SQL `ORDER BY`. Use it to rank locally-built lists (merge results, computed scores).

Single-key only for now; `ThenBy` is not yet available.

Want the **source list itself** reordered rather than a copy? That is `list.Sort()` — see
[List Sort, Reverse and RemoveAt (in place)](https://osysharp.com/reference/function/list-sort/). The two agree about what the order is and differ only in whether your list moved.

`list.Take(n)` returns a **new** `List<T>` of the first `n` elements (in memory). `n` is any integer and is
**clamped** like C# — `n` larger than the list keeps all elements, `n <= 0` yields an empty list, and it
never throws. It chains after `OrderBy` for top-k ranking: `items.OrderByDescending(k).Take(3)`.

## Examples       {#examples}
```osy title="rank a local list" test app=list-orderby
class Scored { public string Id; public decimal Score; }

List<Scored> Top3(List<Scored> items) {
  return items.OrderByDescending(s => s.Score).Take(3);   // top 3 by score; input order kept on ties
}
```

## See also       {#see-also}
- [List Sort, Reverse and RemoveAt (in place)](https://osysharp.com/reference/function/list-sort/) — `Sort`/`Reverse`/`RemoveAt`, which change the list in place instead
- [List indexer](https://osysharp.com/reference/function/list-indexer/) — positional access on the sorted result
- [Text.Split](https://osysharp.com/reference/function/text-split/) — produces a `List<string>` you can sort


---

<!-- https://osysharp.com/reference/function/list-sort/ -->

# List Sort, Reverse and RemoveAt (in place)

> Reorder or shorten a local List<T> IN PLACE, changing the list you are holding rather than answering a new one. Sort() orders by the elements themselves, Reverse() flips the order, RemoveAt(i) deletes by position. The copying counterpart is OrderBy, which leaves the source untouched.

<!-- id: function-list-sort · area: function · stability: stable · html: https://osysharp.com/reference/function/list-sort/ -->

## Summary        {#summary}
`list.Sort()`, `list.Reverse()` and `list.RemoveAt(i)` change **the list you are holding**. They answer
nothing, exactly as their C# counterparts do. That is the whole difference from
[List OrderBy (in-memory)](https://osysharp.com/reference/function/list-orderby/), which answers a **new** list and leaves the source as it was.

Reach for the in-place verb when anything else is holding the same list — a component member, a value
captured in a closure, a list handed to a helper. Those holders see the change. The rewrite people use
when they cannot say `Sort()` — `xs = xs.OrderBy(k).ToList()` — rebinds the *name*, so every other holder
goes on reading the old order.

## Signature      {#signature}
```osy syntax
list.Sort();          // order the list by its elements  → nothing
list.Reverse();       // flip the order                  → nothing
list.RemoveAt(i);     // delete the element at position i → nothing
```

## Description    {#description}
All three are `List<T>` verbs. An array (`T[]`) is fixed-size and has none of them, which is C#'s rule
too — the refusal names `List<T>` and `OrderBy` so the fix is one edit.

**`Sort()` orders by the elements themselves**, so the element type has to be a comparable scalar — a
number, string, date or enum. A list of `class` values has no natural order, and that is a compile error
naming the fix rather than a fault at run time. Sort **by a field** with `OrderBy(x => x.Field)`.

The order is the one `OrderBy` uses, so the two verbs never disagree about *what* the order is; they
differ only in whether your list moved. It is **ordinal, by code point** — not culture-aware, unlike C#'s
own `List<string>.Sort()` — because an order that depends on the machine's locale is not one every engine
can promise. Equal elements keep their written order (a **stable** sort).

**There is no comparer argument.** C#'s `Sort(Comparison<T>)` and `Sort(IComparer<T>)` have no spelling
here — Osy# has neither delegate values nor an `IComparer` type. Sorting by a rule of your own is
`OrderBy(x => key)`, and passing a comparer is refused rather than quietly ignored.

**`Reverse()` on a `List<T>` reverses in place.** Inside a LINQ chain the same word still means the
sequence verb that answers a reversed copy (`list.Reverse().Take(2)`), and on an array it always does —
which is C#'s own split between the instance method and the extension.

**`RemoveAt(i)` deletes by position**; `Remove(x)` deletes by value and `RemoveAll(x => …)` deletes every
match. An index outside the list throws, as in C#.

## Examples       {#examples}
```osy title="the list you are holding is the one that changes" test app=list-sort
List<int> Ranked() {
  var scores = new List<int>();
  scores.Add(30);
  scores.Add(4);
  scores.Add(100);

  var alsoScores = scores;   // a second name for the SAME list
  scores.Sort();             // 4, 30, 100 — and `alsoScores` sees it, because nothing was copied

  alsoScores.RemoveAt(0);    // drop the lowest
  alsoScores.Reverse();      // 100, 30
  return scores;
}
```

```osy title="sorting by a field is OrderBy, and it copies" test app=list-sort-bykey
class Entry { public string Name; public int Score; }

List<Entry> ByScore(List<Entry> entries) {
  // `Sort()` would be refused here: an Entry has no natural order. Name the key instead.
  return entries.OrderByDescending(e => e.Score).ToList();
}
```

## See also       {#see-also}
- [List OrderBy (in-memory)](https://osysharp.com/reference/function/list-orderby/) — the copying counterpart, and how to sort by a key
- [List indexer](https://osysharp.com/reference/function/list-indexer/) — reading and writing a position
- [Sequence fields on a class](https://osysharp.com/reference/class/collections/) — the whole `List<T>` / `HashSet<T>` / `Dictionary<K,V>` surface


---

<!-- https://osysharp.com/reference/function/list-indexer/ -->

# List indexer

> Positional get/set on a List<T> by integer index, exactly like C#'s List<T>.this[int]. The index must be an integer; the result is the element type. Out-of-range access faults at runtime, as in C# — use `ElementAtOrDefault(i)` when the element may legitimately not be there.

<!-- id: function-list-indexer · area: function · stability: stable · html: https://osysharp.com/reference/function/list-indexer/ -->

## Summary        {#summary}
`list[i]` reads, and `list[i] = v` writes, the element at integer position `i` of a `List<T>` — exactly
C#'s `List<T>.this[int]`. The read yields the element type `T`; the write assigns `v` in place. As in C#,
an out-of-range index **faults at runtime** (there is no silent clamp).

## Signature      {#signature}
```osy syntax
list[<int> i]           // read  → T
list[<int> i] = <T> v   // write (in place)
```

## Description    {#description}
The receiver must be a `List<T>` (the mutable list — `new List<T>()` or a `Text.Split` result). The index
expression must be an integer; a non-integer index is a compile error
(`list index must be an integer, got '…'`). Indexing a value that is not indexable is likewise a compile
error.

This complements the collection surface a `List<T>` already exposes — `foreach`, `.Count`, `.Add`,
`.Contains`, `.Remove`. `Dictionary<K,V>` uses the same `[…]` syntax keyed by `K`; a `HashSet<T>` has **no**
indexer (as in C#).

### When the element may not be there — `ElementAtOrDefault(i)`    {#element-at-or-default}
`list[i]` faults past the end, which is right when a missing element means a bug. When it does **not** — reading the
third segment of a path that may only have two — ask for it directly:

```osy title="a segment that may not be there" test app=list-indexer
string SlugOf(string path) {
  var parts = Text.Split(path, "/");        // "/org/acme" -> ["", "org", "acme"]
  return parts.ElementAtOrDefault(2) ?? "";  // "" when the path is shorter
}
```

C#'s own spelling, with C#'s own semantics: the miss yields **null** rather than faulting, and a negative index is a
miss too (never "counting from the end"). The result is **nullable**, so `??` reads naturally and the compiler makes
you say what the miss means.

Prefer it over testing the shape of the input first. A guard like `path.StartsWith("/org/") ? parts[2] : ""` answers a
*different* question than "is there a third segment", and the two drift apart the moment the input shape changes —
whereas guards that carry real meaning (only an `/org/` path *has* a slug) are worth keeping, and this does not replace
them.

## Examples       {#examples}
```osy title="get, set, computed index" test app=list-indexer
string Reorder(string csv) {
  var xs = Text.Split(csv, ",");   // a List<string>
  var first = xs[0];               // read
  xs[0] = xs[xs.Count - 1];        // write, computed index
  xs[xs.Count - 1] = first;        // swap first and last
  return string.Join(",", xs);
}
// Reorder("a,b,c")  ->  "c,b,a"
```

## See also       {#see-also}
- [Text.Split](https://osysharp.com/reference/function/text-split/) — produces a `List<string>` this indexes
- [LINQ over a local list](https://osysharp.com/reference/query/in-memory-linq/) — the LINQ verbs over a list you already hold


---

<!-- https://osysharp.com/reference/function/llm-complete/ -->

# LlmClient.Complete — a model's answer, once

> `LlmClient.Complete(prompt)` asks a model a question and gives you the finished text. Use it when your code wants the answer as a value — to store it, branch on it, or pass it on — and nobody is watching it being written.

<!-- id: function-llm-complete · area: function · stability: preview · html: https://osysharp.com/reference/function/llm-complete/ -->

## Summary        {#summary}
`LlmClient.Complete(prompt)` is the plain model call: text in, text out. It waits for the whole answer and hands it
back as a `string`, so the value can be stored on a record, tested in an `if`, or passed to anything else.

If somebody is *watching* the answer appear, you want [LlmClient.Stream — a model's answer as it is written](https://osysharp.com/reference/function/llm-stream/) instead.

## Signature      {#signature}
```osy syntax
string LlmClient.Complete(string prompt)
string LlmClient.Complete(string prompt, string model)
```

## Description    {#description}
The call returns when the model has finished. That is the whole difference from [LlmClient.Stream — a model's answer as it is written](https://osysharp.com/reference/function/llm-stream/): same model,
same prompt, same cost — the answer simply arrives as one value instead of in pieces.

Reach for `Complete` when the answer is **data your code acts on**, and for `Stream` when the answer is **something a
person reads**. Classifying a support ticket, drafting a field to save, summarising a document into a column — those
want the finished string, and nothing about them is improved by seeing it typed out.

**What it does not do:**
- **No tools, no system prompt, no usage numbers.** This is the raw completion. A richer surface — one that can call
  tools and carry a conversation — belongs with agents.
- **It needs a model provider.** A host that has wired none fails the call rather than returning an empty string: an
  empty answer and a missing provider must not look the same.
- **It does not stream.** A long answer means a long wait, with nothing to show for it until it lands. That is the
  trade, and it is the right one only when nobody is waiting on a screen.

### Choosing the model        {#model}
An app declares its model once (`app.DefaultModel`), and every call uses it. A call that needs a *different* one can
say so:

```osy syntax
string verdict = LlmClient.Complete(ticket, "claude-opus-4-8");
```

⚠ **Naming a model can only NARROW what the app allows.** The name is matched against the models the app admits; if
it is not one of them the call is **refused**, with a message naming what was asked for and what the app allows. It
is never quietly served by the default — a call site cannot know the app's policy, which is exactly why relaxing it
is not the call site's to do. This is the same rule [LlmClient.Stream — a model's answer as it is written](https://osysharp.com/reference/function/llm-stream/) follows, deliberately: which model serves
a call is not a thing you should have to think about differently depending on how you read the answer.

### What does a call cost, and what if I am over budget?        {#cost}
Every call is metered before it is made and recorded after: an app that is over its budget is refused rather than
served, and what the call actually spent is written down against the app.

## Examples       {#examples}
Classifying a ticket and saving the answer:

```osy test app=function-llm-complete-triage
entity Ticket {
  [MaxLength(200)] string Subject;
  [MaxLength(4000)] string Body;
  [MaxLength(40)] string Category;
}

void Triage(Ticket ticket) {
  ticket.Category = LlmClient.Complete(
    "Reply with one word — billing, technical or other. Ticket: " + ticket.Subject);
}
```

## See also       {#see-also}
- [LlmClient.Stream — a model's answer as it is written](https://osysharp.com/reference/function/llm-stream/) — the same call, delivered as it is written
- [function](https://osysharp.com/reference/function/declaration/) — where a server function runs


---

<!-- https://osysharp.com/reference/function/llm-stream/ -->

# LlmClient.Stream — a model's answer as it is written

> `LlmClient.Stream(prompt)` gives you a model's answer in the pieces it was produced in, so a reader sees it being written instead of waiting for it. It can only be read inside a `stream<T>` function, which is what passes those pieces on to whoever is watching.

<!-- id: function-llm-stream · area: function · stability: preview · html: https://osysharp.com/reference/function/llm-stream/ -->

## Summary        {#summary}
A model writes its answer a piece at a time. `LlmClient.Complete(prompt)` hides that — it hands you the finished
text, and until it does, the reader has nothing. `LlmClient.Stream(prompt)` gives you the pieces, so a reader watches
the answer being written.

## Signature      {#signature}
```osy syntax
stream<string> LlmClient.Stream(string prompt)
```

Read only inside a `stream<T>` function, with a `foreach` that passes each piece on.

## Description    {#description}
The whole shape is: read the model's pieces, pass each one on. A component then binds it with an ordinary `live var`
and renders the answer as it appears ([yield — a function that produces results over time](https://osysharp.com/reference/function/yield/)).

**It can only be read inside a `stream<T>` function**, and anywhere else is a compile error that names the wrapper to
write. That is not a style rule. An ordinary `foreach` reads its whole source before the first turn of the loop,
because that list is what the platform writes down if the function pauses in the middle — and a model's answer
cannot be written down half-finished. So the loop that reads one has to be the kind that never pauses, and a
`stream<T>` function is exactly that kind.

The practical version: if you want a model's answer as it arrives, the thing that reads it is a `stream<T>` function.
If you only want the finished text, use `LlmClient.Complete(prompt)`.

**A piece is whatever the model produced in one go** — usually a few characters, sometimes a word or a fragment of
one. Do not treat a piece as a word, a sentence or a token: **concatenate them and you have the answer**, exactly, and
that is all that is promised. The pieces are not word-aligned and they carry their own spacing — a real reply came
back as `[The]` then `[ wire is live]`, with the space leading the second piece — so they join with **no separator**.

**For a rendered reply, put `string.Concat` between the stream and the atom** ([Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/)):
`Markdown(string.Concat(answer), streaming: !answer.Done)`. One atom over the whole answer, not one per piece — a
reply is a single document, and an atom per piece renders a paragraph break at every delta and splits any construct
that spans two of them.

**To STORE the finished answer, use [on settled — run something once, when a stream finishes](https://osysharp.com/reference/ui/on-settled/)** — `on settled(answer) { … }` runs once, when the stream
stops, whichever way it ended. Reach for it rather than the shape it looks like you want: `on change { if
(answer.Done) … }` fires again on every later render, because `Done` stays true once it is true, and in a real app
that stored one reply fourteen times.

**What it does not do:**
- **No tools, no system prompt, no usage numbers.** This is the raw completion, streamed — the same reduction
  `LlmClient.Complete` is of one call. A richer surface belongs with agents.
- **It needs a model provider.** A host that has wired none fails the call rather than streaming nothing: an empty
  answer and a missing provider must not look the same.
- **Stopping the loop stops the generation.** A reader who leaves, or a run the platform drops, stops the pieces
  being pulled — and the model stops producing, so you are not charged for the rest of an answer nobody wanted.
  **You are still charged for the part it had already written.** The provider bills for what it produced whether or
  not anyone was still reading, and the platform records the same — leaving early makes an answer cheaper, never
  free.

### Choosing the model        {#model}
An app declares its model once (`app.DefaultModel`), and every call uses it. A call that needs a *different* one can
say so:

```osy syntax
foreach (var piece in LlmClient.Stream(question, "claude-opus-4-8")) { yield return piece; }
```

⚠ **Naming a model can only NARROW what the app allows.** The name is matched against the models the app admits; if
it is not one of them the call is **refused**, with a message naming what was asked for and what the app allows. It
is never quietly served by the default — a call site cannot know the app's policy, which is exactly why relaxing it
is not the call site's to do.

## Examples       {#examples}
An assistant's reply, from the model to the page:

```osy test app=function-llm-stream-reply
stream<string> Ask(string question) {
  foreach (var piece in LlmClient.Stream(question)) {
    yield return piece;
  }
}

component Reply(string Question) {
  live var answer = Ask(Question);

  render {
    Stack {
      Markdown(string.Concat(answer), streaming: !answer.Done);
      if (answer.Failed) { Text(answer.Error); }
      else if (!answer.Done) { Text("…"); }
    }
  }
}
```

## See also       {#see-also}
- [on settled — run something once, when a stream finishes](https://osysharp.com/reference/ui/on-settled/) — storing the answer once the stream finishes
- [yield — a function that produces results over time](https://osysharp.com/reference/function/yield/) — `stream<T>`, `yield return`, and how a component watches one
- [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) — rendering an answer as it arrives
- [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) — what `live var` binds


---

<!-- https://osysharp.com/reference/function/math/ -->

# Math.Abs, Math.Sign, Math.Min, Math.Max, Math.Clamp, Math.Truncate, Math.Pow, Math.Sqrt, Math.Sin, Math.Cos, Math.Tan

> The numeric helpers, each returning the type C# says it returns. Abs, Min, Max and Clamp answer in the WIDEST of their arguments, so a decimal stays exact and a double stays a double. The rounding family answers a decimal for a decimal and a double otherwise. Pow and Sqrt are always double — C# gives them no other overload. Sign is an int. Truncate rounds toward zero; Clamp throws if its bounds are inverted.

<!-- id: function-math · area: function · stability: stable · html: https://osysharp.com/reference/function/math/ -->

## Summary        {#summary}
`Math.Abs`, `Math.Sign`, `Math.Min`, `Math.Max`, `Math.Clamp`, `Math.Truncate`, `Math.Pow` and `Math.Sqrt`
are the numeric helpers you reach for in a calculation. They join the rounding functions (`Math.Round`,
`Math.Floor`, `Math.Ceiling`), and like those they are pinned to the exact answer this platform's server
produces — which is not always what a bare double would give.

## Signature      {#signature}
```osy syntax
Math.Abs(<number> x)      -> decimal      // magnitude
Math.Sign(<number> x)     -> int          // -1, 0, or 1
Math.Min(<number> a, <number> b) -> decimal
Math.Max(<number> a, <number> b) -> decimal
Math.Clamp(<number> x, <number> min, <number> max) -> decimal
Math.Truncate(<number> x) -> decimal      // drop the fraction, toward zero
Math.Pow(<number> x, <number> y) -> decimal
Math.Sqrt(<number> x)     -> decimal

Math.Sin(<number> radians)  -> double     // the trig family is ALWAYS double
Math.Cos(<number> radians)  -> double
Math.Tan(<number> radians)  -> double
Math.Asin(<number> value)   -> double     // the inverses, answering RADIANS
Math.Acos(<number> value)   -> double
Math.Atan(<number> value)   -> double
Math.Atan2(<number> y, <number> x) -> double   // note the order: y first

Math.Log(<number> value)   -> double      // natural log, base e
Math.Log(<number> value, <number> newBase) -> double   // note the order: VALUE first
Math.Log10(<number> value) -> double
Math.Log2(<number> value)  -> double
Math.Exp(<number> value)   -> double      // e raised to the power — Log's inverse

Math.PI                    -> double      // 3.141592653589793 — a CONSTANT, no parentheses
Math.E                     -> double      // 2.718281828459045
```

## Description    {#description}

### The result type follows the argument, as it does in C#   {#result-type}
There are three rules, and each is the one C#'s own overloads give:

- **`Abs`, `Min`, `Max`, `Clamp` answer in the WIDEST of their arguments.** `Math.Abs(-5)` is an `int`;
  `Math.Abs(-2.5)` is a `double`; `Math.Abs(-2.50m)` is a `decimal`, keeping its digits and its scale. So a
  chain of decimal arithmetic stays exact ([decimal](https://osysharp.com/reference/types/decimal/)) and a chain of double arithmetic stays a double.
- **`Round`, `Floor`, `Ceiling`, `Truncate` answer a `decimal` for a decimal, and a `double` otherwise.** C#
  has both overloads; the `int` case is genuinely ambiguous in C# (it will not compile without a cast) and
  resolves to `double` here.
- **`Pow` and `Sqrt` are always `double`**, and **`Sign` is always an `int`** — a sign is not a quantity.
- **The logarithms — `Log`, `Log10`, `Log2` — and `Exp` are always `double`**, for the same reason: C# gives them
  no decimal overload, and a logarithm feeds straight back into arithmetic, where one decimal operand would promote
  the whole expression.
- **The trig family — `Sin`, `Cos`, `Tan`, `Asin`, `Acos`, `Atan`, `Atan2` — is always `double`**, whatever you
  pass. See below for why that one is not a free choice.

### The trig family is double, and that is load-bearing   {#trig}
`Math.Sin` and friends take an angle in **radians** and answer a `double`; the inverses (`Math.Asin`, `Math.Acos`,
`Math.Atan`, `Math.Atan2`) take a ratio and answer an angle in radians. Degrees are never implied anywhere — convert
them yourself (`degrees * Math.PI / 180`) if that is what you have.

They cannot answer a `decimal`, and the reason is arithmetic rather than taste. A trig result goes straight back into
arithmetic — `dirX * cos - dirY * sin`, every frame — and a single `decimal` operand promotes the whole expression to
decimal. A decimal has a bounded exponent, so a chain of reciprocals over promoted values fails outright with *"value
was either too large or too small for a Decimal"*. Exactness is the right default for money and the wrong tool for a
rotation matrix.

⚠ **`Math.Atan2` takes `(y, x)`, in that order** — C#'s own, and the reverse of what the name suggests on first
reading. It is the one that answers "what bearing is this vector" correctly in all four quadrants, which
`Math.Atan(y / x)` cannot: dividing first throws away the sign information and folds two quadrants onto two others.

### The two constants   {#constants}
`Math.PI` and `Math.E` are **constants, written without parentheses** — the C# spelling. They are `double`, like
everything they feed.

They matter most for the trig family above, which takes radians: an angle is a multiple of π, so before these existed
a degrees-to-radians conversion had to paste the literal `3.141592653589793` at every call site. (This page told you
to do exactly that.)

### The logarithms   {#logarithms}

`Math.Log(x)` is the **natural** log (base e), matching C#. `Math.Log10` and `Math.Log2` are the two bases with
their own methods — prefer them to `Log(x) / Log(10)`, which is one rounding step worse. `Math.Exp` is `Log`'s
inverse, so `Math.Log(Math.Exp(x))` is `x` to within floating-point tolerance.

⚠ **`Math.Log(value, newBase)` takes the VALUE first** — `Math.Log(8, 2)` is `3`, not `⅓`. Both arguments are bare
numbers, so a transposition produces a number rather than an error, and nothing downstream will look wrong.

Every one of these runs on the **client** as well as the server, and lowers into a compiled `on frame` body — a
log-scale axis recomputes one per tick per render, so a round trip for it would be absurd.

⚑ **Comparing these in a test needs `Assert.Equal(expected, actual, precision)`** — the third argument is the number
of decimal places both sides are rounded to. `Math.Sin(Math.PI)` is not exactly 0 and `Math.Log(Math.Exp(2))` is not
exactly 2 in any language, because π and e are not representable; the tolerance is not sloppiness, it is the only
correct way to assert a transcendental.

⚠ **This changed.** These used to return a `decimal` whatever you passed. It was a bug rather than a policy:
the type system already declared the rules above and only the runtimes disagreed. If you were relying on
`Math.Floor(someInt)` handing back a decimal, it now hands back a double of the same value.

**Why it mattered.** A `decimal` operand promotes its whole expression, so a single `Math.Abs` inside otherwise
`double` arithmetic quietly turned the rest of the calculation into decimal — and a decimal has a bounded
exponent, so a chain of reciprocals eventually failed with *"value was either too large or too small for a
Decimal"*. Exactness is what you want for money and the wrong tool for geometry.

### Abs, Sign, and Truncate   {#abs-sign-truncate}
`Math.Abs(x)` is the magnitude; `Math.Sign(x)` is `-1`/`0`/`1`. `Math.Truncate(x)` drops the fraction
**toward zero**, so `Math.Truncate(-2.9)` is `-2` — this is the difference from `Math.Floor`, which goes
toward negative infinity and gives `-3`. For a value already positive the two agree.

### Min, Max, and Clamp   {#min-max-clamp}
`Math.Min`/`Math.Max` return the smaller/larger of two numbers. `Math.Clamp(x, min, max)` pins `x` into the
`[min, max]` range — below `min` it returns `min`, above `max` it returns `max`. If you invert the bounds so
`min > max`, `Math.Clamp` **throws**: there is no range to clamp into, and returning a silent wrong value
would be worse. (When two values are numerically equal but differ in scale — `1.5` vs `1.50` — which one
`Min`/`Max` returns is fixed but rarely matters; use a format specifier if the displayed scale is important.)

### Pow and Sqrt are doubles, and are not exact   {#pow-sqrt}
`Math.Pow(x, y)` and `Math.Sqrt(x)` have **no exact decimal form** for most inputs — C# gives them no decimal
overload at all — so they are computed in binary floating point and **stay** there. `Math.Sqrt(2)` is
`1.4142135623730951`, the full float64 answer, identical in the browser and on the server.

Do not use them where you need the last cent to be provably right: a square root of a price is not a price.
Convert deliberately if you need to come back to money.

These are pure functions of their arguments, so they run **in the browser** with no round trip where a UI
action needs them ([execution side](https://osysharp.com/reference/function/execution-side/)).

## Examples       {#examples}
```osy title="a bounded, rounded score" test app=math
// Clamp a raw score into range, then keep two decimals for display.
decimal Score(decimal raw) {
  return Math.Round(Math.Clamp(raw, 0m, 100m), 2);
}
```

```osy title="the exact answers, pinned" run app=math
[Test]
void Math_answers() {
  Assert.Equal(87.35m, Score(87.347m));
  Assert.Equal(100m, Score(140m));            // clamped to the ceiling
  Assert.Equal(0m, Score(-5m));               // clamped to the floor

  Assert.Equal(5, Math.Abs(-5));              // int in, INT out — the widest argument wins
  Assert.Equal(2.50m, Math.Abs(-2.50m));      // decimal in, decimal out — scale kept
  Assert.Equal(1, Math.Sign(2.5m));           // Sign returns an int
  Assert.Equal(0, Math.Sign(0m));
  Assert.Equal(-1, Math.Sign(-0.0001m));

  Assert.Equal(-2m, Math.Truncate(-2.9m));    // toward zero…
  Assert.Equal(-3m, Math.Floor(-2.9m));       // …unlike Floor

  Assert.Equal(3, Math.Min(3, 7));
  Assert.Equal(7, Math.Max(3, 7));
  Assert.Equal(3, Math.Clamp(5, 1, 3));

  Assert.Equal(1024.0, Math.Pow(2, 10));      // Pow and Sqrt are DOUBLES, always
  Assert.Equal(4.0, Math.Sqrt(16));

  Assert.Equal(3.0, Math.Log10(1000));        // the logs are doubles too
  Assert.Equal(3.0, Math.Log2(8));
  Assert.Equal(3.0, Math.Log(8, 2));          // VALUE first — Log(2, 8) would be 0.333…
  Assert.Equal(1.0, Math.Exp(0));

  // The constants — no parentheses.
  Assert.Equal(3.141592653589793, Math.PI);
  Assert.Equal(2.718281828459045, Math.E);

  // …and the functions, which are never EXACTLY their mathematical answer: the third argument is the number of
  // decimal places both sides are rounded to before comparing.
  Assert.Equal(0.0, Math.Sin(Math.PI), 12);
  Assert.Equal(2.0, Math.Log(Math.Exp(2)), 12);
}
```

### Can I pass a string to `Math.*`?   {#string-arguments}
`Math.*` will coerce a **string** argument, and it reads it with the platform's one numeric-coercion grammar —
the same one [Convert](https://osysharp.com/reference/function/convert/) documents, including its `0` for text it cannot read. `Math.Abs("-1,000")` is
`1000`; `Math.Abs("(5)")` is `0`, not `5`. If the value came from a form field, convert it deliberately first
(`Convert.ToDecimal`) so the failure is visible where it happens rather than inside the arithmetic.

## See also       {#see-also}
- [decimal](https://osysharp.com/reference/types/decimal/) — what a decimal argument buys you, and where exactness matters
- [format specifiers](https://osysharp.com/reference/function/format-specifiers/) — rounding for DISPLAY (`F2`, `N0`), distinct from `Math.Round`
- [Convert](https://osysharp.com/reference/function/convert/) — coercing between numeric types


---

<!-- https://osysharp.com/reference/function/numeric-literals/ -->

# Numeric types & literal suffixes

> The platform numeric types are int, long, decimal, and double. Literals follow C# exactly, suffixes (L, m, d) included: a bare decimal-point literal is a DOUBLE, and money is written 19.99m. There is no float: 2.5f is a pointed compile error.

<!-- id: function-numeric-literals · area: function · stability: stable · html: https://osysharp.com/reference/function/numeric-literals/ -->

## Summary        {#summary}
The platform numeric types are `int` (Int32), `long` (Int64), `decimal`, and `double`. Numeric literals
follow C#, with no divergence at all: a bare integer is `int`, a bare decimal-point literal (`2.5`) is a
**`double`**, `L` makes a `long`, `m` a `decimal`, `d` a `double`. There is no `float` type; `2.5f` is a
pointed compile error.

⚠ **This page said the opposite until 2026-08-31**, describing a bare `2.5` as a decimal "money-safe
default". That divergence was real and was **removed on 2026-08-12**; the page was not updated with it, so
it taught a spelling the compiler rejects — `decimal Price() { return 19.99; }` does not compile.

## Signature      {#signature}
```osy syntax
5              // int
5000000000L    // long  (values past int.MaxValue need the suffix)
2.5            // double — same as C#, and same as every other unsuffixed fractional literal
2.5m           // decimal — what money is written as
2.5d           // double
```

## Description    {#description}
- **No `float`.** The platform numeric model has no float type; `2.5f` refuses with
  `float literals … are not supported — the platform numeric types are int/long/decimal/double`.
- **Bare `2.5` is a `double`, exactly as in C#.** It was a decimal until 2026-08-12, and that was the one
  place Osy#'s literals disagreed with the language they mirror — a disagreement invisible until arithmetic
  overflows. A canvas whose fields were all `double` ran entirely in decimal because every literal
  initialising them was one, and threw "value was either too large or too small for a Decimal" naming a type
  the source never wrote.
- **Money is safe BECAUSE of this, not despite it.** C#'s rule is a PAIR: `1.0` is a double AND there is no
  implicit `double`→`decimal`. Osy# keeps both halves, so `decimal price = 19.99;` is a compile error and the
  author writes `19.99m` — strictly safer than a default that made every literal a decimal whether the
  surrounding expression wanted one or not.
- **Arithmetic runs in the decimal lattice** (matching Postgres): a `double` operand behaves like any
  non-integral number — `5d / 2 == 2.5` (a double defeats integer truncation, as in C#), while
  int÷int truncates (`5 / 2 == 2`, SQL parity).
- **Round-trip:** suffixed literals persist as typed literal nodes and decompile ALWAYS suffixed
  (`5000000000L`, `5d`), so the canonical form re-lexes identically.
- **Durable:** across a suspend/resume, a `double` local resumes as `decimal` (the codec's numeric
  normalization — the same value under the decimal arithmetic lattice).
- `const` composes: `const double factor = 1.5d;` folds and inlines like every const.

## Examples       {#examples}
```osy title="suffixes" test app=numeric-literals
long Big() { return 5000000000L; }      // past int.MaxValue — needs the L
double Half() { return 5d / 2; }        // == 2.5 — the double defeats integer truncation
decimal Price() { return 19.99m; }      // money takes the m — a bare 19.99 is a double and will not convert
double Rate() { const double f = 1.5d; return f; }
```

## See also       {#see-also}
- [Typed locals](https://osysharp.com/reference/function/typed-locals/) — declared types + the constant conversion (`double h = 2.5;` works)
- [const](https://osysharp.com/reference/function/const/) — const folding and inlining


---

<!-- https://osysharp.com/reference/function/date-parts/ -->

# Reading a date — Year, Month, Day, Hour, Minute, Second, DayOfWeek, Date

> Read a component off a DateTime — its year, month, day, hour, minute, second — or its day of the week (Sunday is 0), or truncate it to midnight with .Date. The same part-readers work on a DateOnly and a TimeOnly. All are pure and run in the browser.

<!-- id: function-date-parts · area: function · stability: stable · html: https://osysharp.com/reference/function/date-parts/ -->

## Summary        {#summary}
Given a `DateTime`, these read one component out of it: `Year`, `Month`, `Day`, `Hour`, `Minute`, `Second`
(each an `int`), `DayOfWeek` (an `int`, with **Sunday = 0**), and `Date` (the same instant truncated to
**midnight**). The same readers also work on a [TimeSpan, DateOnly, TimeOnly](https://osysharp.com/reference/types/duration-and-parts/) `DateOnly` or `TimeOnly`.

## Signature      {#signature}
```osy syntax
d.Year / d.Month / d.Day        -> int
d.Hour / d.Minute / d.Second    -> int
d.DayOfWeek                     -> int      // Sunday = 0, Monday = 1, … Saturday = 6
d.Date                          -> DateTime // same date, time set to 00:00:00
```

## Description    {#description}
Each reader pulls a single field out of the value. `Month` is **1-based** (January is `1`, not `0` — unlike a
JavaScript `Date`), and `Day` is the day of the month.

**`DayOfWeek` counts from Sunday.** `Sunday` is `0`, `Monday` is `1`, up to `Saturday` at `6` — so
`2024-03-15`, a Friday, gives `5`.

**`Date` truncates to midnight.** It returns a `DateTime` on the same calendar day with the time cleared to
`00:00:00`, which is how you compare two timestamps "on the same day" or bucket by day.

**The same readers work on a `DateOnly` and a `TimeOnly`.** `someDate.Month` reads the month off a `DateOnly`;
`someTime.Hour` reads the hour off a `TimeOnly` — the part-reader widens to whichever value you give it.

The member syntax `d.Month` is the everyday spelling; the compiler knows these readers as `Date.Year`,
`Date.Month`, `Date.Day`, `Date.Hour`, `Date.Minute`, `Date.Second`, `Date.DayOfWeek` and `Date.Date`, and
they can also be written in that call form (`Date.Month(d)`).

All of these are pure functions of the value, so they run **in the browser** with no round trip
([execution side](https://osysharp.com/reference/function/execution-side/)). For the current instant to read them off, see [Current time (DateTime.UtcNow, DurableClock.Now)](https://osysharp.com/reference/function/current-time/).

### The whole instant as one number — `Date.Ticks`   {#ticks}
When you want an instant as a single comparable/​storable number rather than as parts, `Date.Ticks(d)` answers it —
one number that orders the same way the instants do. Use it for an ordering key or a compact stamp, not for
arithmetic you could write with the date operators themselves:

```osy title="an instant as one orderable number — and it is a `long`" syntax
long stamp = Date.Ticks(DateTime.UtcNow);
```

⚠ **It is a `long`, not an `int`** — the tick count passed `int`'s range in 1970, so a variable or a field holding
one must say `long`.

## Examples       {#examples}
```osy title="is a timestamp on a weekend?" test app=date-parts
bool IsWeekend(DateTime d) {
  return d.DayOfWeek == 0 || d.DayOfWeek == 6;   // Sunday is 0, Saturday is 6
}
```

```osy title="the exact answers, pinned" run app=date-parts
[Test]
void Date_parts() {
  var d = DateTime.New(2024, 3, 15, 13, 45, 30);
  Assert.Equal(2024, d.Year);
  Assert.Equal(3, d.Month);             // 1-based
  Assert.Equal(15, d.Day);
  Assert.Equal(13, d.Hour);
  Assert.Equal(45, d.Minute);
  Assert.Equal(30, d.Second);
  Assert.Equal(5, d.DayOfWeek);         // Friday — Sunday is 0
  Assert.False(IsWeekend(d));           // Friday is not a weekend…
  Assert.True(IsWeekend(d.AddDays(2))); // …but the Sunday two days later is

  // .Date truncates the time to midnight, same calendar day
  Assert.Equal(0, d.Date.Hour);
  Assert.Equal(15, d.Date.Day);

  // the same readers widen to a DateOnly and a TimeOnly
  Assert.Equal(3, DateOnly.New(2024, 3, 15).Month);
  Assert.Equal(13, TimeOnly.New(13, 45, 30).Hour);
}
```

## See also       {#see-also}
- [Date arithmetic — AddDays, AddMonths, AddYears, AddHours, AddMinutes](https://osysharp.com/reference/function/date-arithmetic/) — adding days/months/years, and its calendar clamping
- [Current time (DateTime.UtcNow, DurableClock.Now)](https://osysharp.com/reference/function/current-time/) — reading the current instant to pull parts from
- [DateTime](https://osysharp.com/reference/types/datetime/) — the `DateTime` type and how it is stored


---

<!-- https://osysharp.com/reference/function/regex/ -->

# Regex

> Matching, replacing and splitting with regular expressions. A pattern means the same thing wherever your code runs — the platform makes the browser reproduce the server's regex semantics exactly.

<!-- id: function-regex · area: function · stability: stable · html: https://osysharp.com/reference/function/regex/ -->

## Summary        {#summary}

Matching, replacing and splitting with regular expressions. A pattern means the same thing wherever your code runs —
the platform makes the browser reproduce the server's regex semantics exactly, so you never have to think about which
engine evaluated it.

## Signature      {#signature}

```osy syntax
bool     Regex.IsMatch(string input, string pattern)
string   Regex.Replace(string input, string pattern, string replacement)
string[] Regex.Split(string input, string pattern)
```

## Description    {#description}

Osy# regular expressions follow **.NET semantics**, everywhere. That sentence is doing more work than it looks.

Browsers and servers do not natively agree about regular expressions. They are different dialects, and — this is the
dangerous part — they mostly disagree *silently*. The same pattern compiles in both and quietly matches different
things:

| Pattern | Input | .NET | Browser (natively) |
|---|---|---|---|
| `^\d+$` | `٣٤` | matches | does **not** match |
| `^\w+$` | `café` | matches | does **not** match |
| `a$` | `"a\n"` | matches | does **not** match |

`\d` means *any decimal digit* in .NET and *`[0-9]`* in a browser. `\w` means *any word character* in .NET and
*`[A-Za-z0-9_]`* in a browser. And the trap is not in exotic patterns — it is in ordinary ones. `^[A-Z]{3}-\d{4}$`
looks completely safe, and diverges the moment somebody types Arabic-Indic digits.

**You do not have to care.** The compiler rewrites your pattern so the browser reproduces .NET's behaviour, and both
engines are tested against each other on every build. `Regex.IsMatch(name, @"^\w+$")` gives the same answer in an
action running in the browser as it does in a function running on the server.

### When a pattern stays on the server   {#server-only}

A few .NET constructs have no equivalent in a browser at all — atomic groups (`(?>…)`), conditionals (`(?(…)…)`), and
character-class subtraction (`[a-z-[aeiou]]`). And a pattern *built at runtime* from a variable cannot be examined
ahead of time.

In those cases the function simply runs on the server, where .NET evaluates it. Your code is unchanged, the answer is
correct, and the only cost is a round trip. Nothing to configure.

### A note on very complex patterns   {#backtracking}

A pathological pattern can be made to backtrack catastrophically. On the server there is a timeout that stops this. In
a browser there is not — a runaway pattern will hang the tab it is running in. If you are matching against input a user
controls, prefer a simple, anchored pattern.

## Examples       {#examples}

```osy title="validating a code" test app=regex-basics
bool IsOrderCode(string code) {
  return Regex.IsMatch(code, "^[A-Z]{3}-[0-9]{4}$");
}
```

```osy title="masking" test app=regex-basics
string MaskDigits(string s) {
  return Regex.Replace(s, "[0-9]", "*");
}
```

## See also       {#see-also}
- [execution side](https://osysharp.com/reference/function/execution-side/) — where a function runs, and why a regex does not force the choice


---

<!-- https://osysharp.com/reference/function/crypto-bytes/ -->

# Signing with raw bytes — Crypto.HmacSha256, Sha256, ToHex, and Text.ToBytes

> The byte-in, byte-out half of the crypto surface. Real request-signing schemes chain their HMACs — each step's raw output becomes the next step's key — and a hex-returning HMAC cannot do that, because the hex text of a digest is not the digest. These take and answer bytes, so the chain composes; hex appears once, at the end.

<!-- id: function-crypto-bytes · area: function · stability: stable · html: https://osysharp.com/reference/function/crypto-bytes/ -->

## Summary        {#summary}
[`Crypto.HmacSha256Hex`](https://osysharp.com/reference/function/crypto-hmac/) answers **hex**, which is right when the tag is the final answer —
verifying a webhook, signing a cookie. It is the wrong shape for a **signing chain**, and that is what this page is
for.

`Crypto.HmacSha256(key, message)` takes bytes and answers bytes, so **its own output is a legal key for the next
call**. `Text.ToBytes` gets you bytes from text, and `Crypto.ToHex` renders the final result.

## Signature      {#signature}
```osy syntax
byte[] Crypto.HmacSha256(byte[] key, byte[] message)   // keyed, raw in and raw out — this is the chaining one
byte[] Crypto.Sha256(byte[] data)                      // unkeyed digest, raw
string Crypto.ToHex(byte[] bytes)                      // lowercase hex
byte[] Crypto.FromHex(string hex)                      // back again; either case in

byte[] Text.ToBytes(string s)                          // UTF-8
string Text.FromBytes(byte[] bytes)                    // UTF-8
```

## Description    {#description}

### Why hex cannot chain      {#why-bytes}
A scheme like AWS Signature Version 4 derives its signing key in four steps, and **each step's raw output is the next
step's key**:

```text
kDate     = HMAC("AWS4" + secret, date)
kRegion   = HMAC(kDate,   region)
kService  = HMAC(kRegion, service)
kSigning  = HMAC(kService, "aws4_request")
signature = hex(HMAC(kSigning, stringToSign))
```

Feed the *hex text* of `kDate` forward and every later step is keyed on the wrong 64 bytes. Nothing local objects —
the code reads correctly, each call succeeds, and the only symptom is that the far end answers `403`. That is why
these exist as a separate, byte-typed surface rather than as another string overload: the type is what stops the
mistake.

### Text and bytes are different things   {#text-and-bytes}
`Text.ToBytes` encodes as **UTF-8**; `Text.FromBytes` decodes the same way. Everything here is strict about which one
it takes — passing a `string` where a `byte[]` is wanted is a compile error naming both, rather than a silent encode.

⚠ `Text.FromBytes` is for bytes you know are text. Bytes that are not — an image, a digest — have no meaningful text
form; use [[function-crypto-bytes#signature|`Crypto.ToHex`]] or `Convert.ToBase64String` to render those.

## Examples       {#examples}
The SigV4 derivation, written line-for-line from the spec above:

```osy title="AWS SigV4's signing key and signature" test app=crypto-bytes
string SigV4Signature(string secret, string dateStamp, string region, string service, string stringToSign) {
  var kSecret  = Text.ToBytes("AWS4" + secret);
  var kDate    = Crypto.HmacSha256(kSecret,  Text.ToBytes(dateStamp));
  var kRegion  = Crypto.HmacSha256(kDate,    Text.ToBytes(region));
  var kService = Crypto.HmacSha256(kRegion,  Text.ToBytes(service));
  var kSigning = Crypto.HmacSha256(kService, Text.ToBytes("aws4_request"));
  return Crypto.ToHex(Crypto.HmacSha256(kSigning, Text.ToBytes(stringToSign)));
}
```

A signed request also carries a hash of its **payload**, which is usually not text:

```osy title="hash the request payload" test app=crypto-bytes
string PayloadHash(byte[] body) {
  return Crypto.ToHex(Crypto.Sha256(body));
}
```

## See also       {#see-also}
- [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/) — the hex-returning HMAC, for when the tag IS the answer, and `FixedTimeEquals`
- [Crypto.Sha256Hex](https://osysharp.com/reference/function/crypto-sha256hex/) — the unkeyed hash over text
- [Http.*](https://osysharp.com/reference/http/facade/) — `Http.Put(url, bytes, contentType)`, which is what a signed request is usually attached to


---

<!-- https://osysharp.com/reference/function/string-interpolation/ -->

# String interpolation & format specifiers

> Build a string from literal text and embedded expressions with $"…{expr}…". A hole may carry a .NET format specifier after a colon ({amount:F2}), applied via IFormattable in InvariantCulture — exactly C#.

<!-- id: function-string-interpolation · area: function · stability: stable · html: https://osysharp.com/reference/function/string-interpolation/ -->

## Summary        {#summary}
`$"…{expr}…"` builds a string from literal text and embedded expressions. A hole may carry a .NET **format
specifier** after a colon — `{amount:F2}` — applied via `IFormattable` in `InvariantCulture`, exactly like
C#'s `string.Format`.

## Signature      {#signature}
```osy syntax
$"literal {expr} more {expr:format} text"
```

## Description    {#description}
- Each `{expr}` hole is converted to its string form and concatenated with the surrounding literal text.
- A `:format` after the expression applies a .NET format string to a formattable value (numbers, dates):
  `{total:F2}` → two decimals, `{n:N0}` → thousands separators, `{ratio:P1}` → a percent, `{when:yyyy-MM-dd}`
  → a date. Formatting always uses `InvariantCulture` (deterministic across hosts).
- A **non-formattable** value (a `string`) ignores the specifier and coerces as usual — matching C#'s
  `string.Format`.
- The formatted conversion is also available directly as the 2-arg `Convert.ToString(value, "F2")`.

## Examples       {#examples}
```osy title="format specifiers" test app=interpolation-format
string Money(decimal amount) { return $"Total: {amount:F2}"; }   // "Total: 1234.50"
string Thousands(int n) { return $"{n:N0}"; }                    // "1,234,567"
string Percent(decimal ratio) { return $"{ratio:P1}"; }          // "12.3 %"
string Plain(int n) { return $"n = {n}"; }                       // "n = 5" (no specifier)
string Direct(decimal d) { return Convert.ToString(d, "F3"); }   // "3.142"
```

## See also       {#see-also}
- [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) — the numeric values you format
- [Convert](https://osysharp.com/reference/function/convert/) — `Convert.ToString` and the coercion builtins


---

<!-- https://osysharp.com/reference/function/text-bytesize/ -->

# Text.ByteSize

> Formats a byte count as the words a person reads — "0 B", "1.5 KB", "2.7 MB". Binary units (1024) with the conventional KB/MB/GB labels. An ordinary function, so the same call formats a size in a grid cell, a detail line or a tooltip.

<!-- id: function-text-bytesize · area: function · stability: stable · html: https://osysharp.com/reference/function/text-bytesize/ -->

## Summary        {#summary}
A file's size is stored as a number of bytes and read by a person as words. `Text.ByteSize` converts one to the other:

```osy title="a size a person can read" test app=text-bytesize
string SizeLabel(int bytes) {
  return Text.ByteSize(bytes);     // 2831155 → "2.7 MB"
}
```

## Signature      {#signature}
```osy syntax
string Text.ByteSize(<int> bytes)
```

## Description    {#description}
Units are **binary** (1024 per step) with the conventional labels — `B`, `KB`, `MB`, `GB`, `TB`, `PB` — which is what
`du -h`, docker and node print, and what a developer reading a file size expects.

Whole bytes read as an integer; everything above carries one decimal:

| bytes | reads as |
|---|---|
| `0` | `0 B` |
| `999` | `999 B` |
| `1024` | `1.0 KB` |
| `1536` | `1.5 KB` |
| `2831155` | `2.7 MB` |

A negative count is clamped to `0 B` — a negative size is not a thing, and `-5 B` would only ever be a bug showing
through. The answer is identical on the server and in the browser, so the same expression is safe wherever it runs.

### Why a function, not a column setting   {#why-a-function}
A size is not only ever shown in a grid. The same string belongs in a detail line, a tooltip, a confirmation message —
so formatting lives in a **function you call**, not in a format flag on some control's contract. A grid renders one with
an ordinary [cell template](https://osysharp.com/reference/ui/controls/):

```osy syntax
Grid(rows: files, columns: [ new GridColumn { Key = "Size", Label = "Size", Align = "right" } ]) { f =>
  slot Size { f => Text(Text.ByteSize(f.Size)); }
}
```

The alternative — a `Format = "bytes"` marker on the column — would have to be re-invented on every surface that ever
shows a size, and the next format after it (durations, percentages, compact counts) would each need their own marker.
A function composes; a marker is a catalogue.

## See also       {#see-also}
- [Convert](https://osysharp.com/reference/function/convert/) — the general value → string conversions
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — cell templates, where a grid calls this


---

<!-- https://osysharp.com/reference/function/text-transform/ -->

# Text.Capitalize, Text.Replace, Text.Repeat, Text.Left, Text.Right

> Produce a new string from an old one: upper-case the first letter, replace every occurrence of a substring, repeat it, or take characters from one end. Replace hits every non-overlapping occurrence left-to-right and throws on an empty search; Left/Right clamp rather than throw. All run in memory.

<!-- id: function-text-transform · area: function · stability: stable · html: https://osysharp.com/reference/function/text-transform/ -->

## Summary        {#summary}
Each of these returns a **new** string. `Text.Capitalize(s)` upper-cases the first character and leaves the
rest alone. `Text.Replace(s, old, new)` swaps every occurrence of `old` for `new`. `Text.Repeat(s, n)`
concatenates `s` with itself `n` times. `Text.Left(s, n)` and `Text.Right(s, n)` take `n` characters from
the start or end.

## Signature      {#signature}
```osy syntax
Text.Capitalize(<string> s) -> string             // first character upper, rest unchanged
Text.Replace(<string> s, <string> old, <string> new) -> string   // EVERY occurrence
Text.Repeat(<string> s, <int> n) -> string
Text.Left(<string> s, <int> n)  -> string         // first n characters
Text.Right(<string> s, <int> n) -> string         // last n characters
```

## Description    {#description}

### Capitalize touches only the first character   {#capitalize}
`Text.Capitalize` upper-cases the **first** character and copies the rest verbatim — it does **not**
lower-case the tail, and it does **not** touch other words. `Text.Capitalize("hELLO wORLD")` is
`"HELLO wORLD"`. If you want each word title-cased with its tail lowered, that is a different function —
[Text.TitleCase](https://osysharp.com/reference/function/text-titlecase/). The casing is invariant simple-case mapping, so a ligature like `ﬁ` is left as
it is rather than expanded.

### Replace hits EVERY occurrence, left-to-right, non-overlapping   {#replace}
`Text.Replace("a-b-c", "-", "+")` is `"a+b+c"`. Matching is greedy and left-to-right with no overlap, so
`Text.Replace("aaa", "aa", "b")` is `"ba"` — the first `"aa"` is replaced, leaving a trailing `"a"`. The
search is a **literal**, not a pattern: `Text.Replace("a.b", ".", "-")` replaces the actual dot. An **empty**
`old` **throws** (there is nothing to find) — guard it if the search text is user-supplied.

### Repeat and Left/Right clamp, they do not throw   {#clamping}
`Text.Repeat(s, 0)` and a negative count both yield `""`. `Text.Left`/`Text.Right` **clamp**: asking for
more characters than the string has returns the whole string, and a negative count returns `""` — neither
throws. (This is unlike `Text.Substring`, which throws when its range runs past the end.)

All of these run **in memory** on a value already in hand.

## Examples       {#examples}
```osy title="build a short, tidy label" test app=text-transform
// Capitalise, then keep it short with an ellipsis if it overruns.
string ShortLabel(string raw) {
  var c = Text.Capitalize(raw);
  if (Text.Length(c) <= 8) { return c; }
  return Text.Left(c, 7) + "…";
}
```

```osy title="the exact answers, pinned" run app=text-transform
[Test]
void Text_transform_answers() {
  Assert.Equal("Hello", ShortLabel("hello"));
  Assert.Equal("Announc…", ShortLabel("announcement"));   // clamped Left + ellipsis

  Assert.Equal("HELLO wORLD", Text.Capitalize("hELLO wORLD"));   // only the first char
  Assert.Equal("a+b+c", Text.Replace("a-b-c", "-", "+"));
  Assert.Equal("ba", Text.Replace("aaa", "aa", "b"));            // greedy, non-overlapping
  Assert.Equal("a-b", Text.Replace("a.b", ".", "-"));            // the dot is literal

  Assert.Equal("ababab", Text.Repeat("ab", 3));
  Assert.Equal("", Text.Repeat("ab", -2));                       // clamps to empty

  Assert.Equal("He", Text.Left("Hello", 2));
  Assert.Equal("lo", Text.Right("Hello", 2));
  Assert.Equal("Hello", Text.Left("Hello", 99));                 // clamps, never throws
  Assert.Equal("", Text.Right("Hello", -1));
}
```

## See also       {#see-also}
- [Text.TitleCase](https://osysharp.com/reference/function/text-titlecase/) — capitalise EACH word (and lower-case the tails), unlike `Capitalize`
- [Text.LastIndexOf](https://osysharp.com/reference/function/text-lastindexof/) — `Text.Substring`, which THROWS out of range where `Left`/`Right` clamp
- [Text.Length, Text.IsEmpty, Text.IsBlank, Text.Contains, Text.StartsWith, Text.EndsWith](https://osysharp.com/reference/function/text-inspect/) — `Text.Length`, `Text.Contains`, and the emptiness checks


---

<!-- https://osysharp.com/reference/function/text-concat/ -->

# Text.Concat

> Joins a list of values into one string with NOTHING between the elements. It is Text.Join with no separator, and the same operation as string.Concat. A single non-list value is converted on its own. Runs in memory.

<!-- id: function-text-concat · area: function · stability: stable · html: https://osysharp.com/reference/function/text-concat/ -->

## Summary        {#summary}
`Text.Concat(values)` runs the elements of `values` together into one string, with **nothing** between them. It is
[Text.Join](https://osysharp.com/reference/function/text-join/) with an empty separator, and identical to `string.Concat(values)`.

Reach for it when the pieces already carry whatever punctuation they need — path segments that end in `/`, pre-formatted
fragments, an accumulated list of chunks. When you want something *between* the elements, use `Text.Join`.

## Signature      {#signature}
```osy syntax
Text.Concat(<list> values)  -> string
string.Concat(<list> values) -> string   // the same operation
```

Each element is converted to its string form first, exactly as string interpolation would convert it.

## Description    {#description}

### It is `Text.Join` with no separator   {#vs-join}
The two are the same operation and differ only in what falls between the elements — nothing, versus the separator you
name. `Text.Concat(parts)` and `Text.Join("", parts)` produce identical output; prefer `Concat`, which says the
intent without an empty-string argument to read past.

### An empty list yields the empty string   {#empty-list}
There is nothing to run together, so the result is `""` — not null. That means a `Concat` over a filtered list stays
safe when the filter matched nothing, with no length check first.

### A single value is converted on its own   {#single-value}
Passed one non-list value, it converts that value and returns it. That makes it usable as a plain "to string" in code
that sometimes holds a list and sometimes holds one item, without branching.

### It runs in memory, on whichever side the cursor is already on   {#execution-side}
Like the rest of `Text.*`, it is a pure in-memory operation — no round trip, and no hand-off. A component may call it
while rendering; a server function may call it mid-body. See [execution side](https://osysharp.com/reference/function/execution-side/).

## Examples       {#examples}

```osy title="running pre-punctuated pieces together" test app=text-concat
entity Doc {
  [Required, MaxLength(200)] string Title;
  security { allow read when IsAuthenticated || IsAnonymous; allow create, update when IsAuthenticated || IsAnonymous; }
}

// The segments already carry their own separators, so anything BETWEEN them would be wrong.
string BuildPath(string[] segments) {
  return Text.Concat(segments);
}

// An empty list is the empty string — no length check needed before calling.
string Nothing() {
  return Text.Concat([]);
}
```

## See also       {#see-also}
- [Text.Join](https://osysharp.com/reference/function/text-join/) — the same operation with something between the elements
- [String interpolation & format specifiers](https://osysharp.com/reference/function/string-interpolation/) — `$"{a}{b}"`, which is usually clearer for a fixed, known set of pieces
- [Text.Split](https://osysharp.com/reference/function/text-split/) — the inverse direction


---

<!-- https://osysharp.com/reference/function/text-indexof/ -->

# Text.IndexOf

> The C# string.IndexOf: the index of the FIRST ordinal occurrence of a substring, or -1 if absent. The 3-arg overload resumes the forward search at startIndex, which is how you walk a string one match at a time. Runs in memory only — there is no SQL push-down form.

<!-- id: function-text-indexof · area: function · stability: stable · html: https://osysharp.com/reference/function/text-indexof/ -->

## Summary        {#summary}
`Text.IndexOf(s, sub)` returns the zero-based index of the **first** ordinal occurrence of `sub` in `s`,
or `-1` when `sub` does not occur — exactly C#'s `string.IndexOf`. The 3-arg overload
`Text.IndexOf(s, sub, startIndex)` begins the search **at** `startIndex` and runs forward, matching
C#'s `IndexOf(value, startIndex)`.

## Signature      {#signature}
```osy syntax
Text.IndexOf(<string> s, <string> sub) -> int
Text.IndexOf(<string> s, <string> sub, <int> startIndex) -> int
```

## Description    {#description}
Matching is **ordinal** (byte-for-byte, culture-independent), the same as [Contains, StartsWith, EndsWith](https://osysharp.com/reference/function/string-search/)'s
`Contains`. A miss returns `-1`. `Text.LastIndexOf` ([Text.LastIndexOf](https://osysharp.com/reference/function/text-lastindexof/)) is the backward-search
sibling.

The 3-arg `startIndex` is the C# contract: it is the *first* position considered, and the search runs
forward from it. `startIndex` may run from `0` to the string's length **inclusive** — a start exactly at
the end is legal and simply finds nothing — and anything outside that range faults rather than clamping.

⚠ **Dropping the `startIndex` is not a simplification.** The whole point of the overload is "find the next
one after the one I already found", so a search that restarts at `0` answers a position the caller has
already passed — and code that then compares that position against where it was looking takes the wrong
branch on every input, silently.

`Text.IndexOf` is **in-memory only** — like `Text.Reverse`, it has no faithful SQL form, so it may be
called on locals inside a function body but not pushed down into a query predicate.

## Examples       {#examples}
```osy title="walking a string one match at a time" test app=text-indexof
string SecondField(string line) {
  var first = Text.IndexOf(line, ",");
  if (first < 0) {
    return "";
  }
  var second = Text.IndexOf(line, ",", first + 1);   // resume AFTER the one just found
  if (second < 0) {
    return Text.Substring(line, first + 1);
  }
  return Text.Substring(line, first + 1, second - first - 1);
}
// SecondField("a,b,c")  ->  "b"
```

## See also       {#see-also}
- [Text.LastIndexOf](https://osysharp.com/reference/function/text-lastindexof/) — the backward-search sibling, with the same `startIndex` contract
- [Contains, StartsWith, EndsWith](https://osysharp.com/reference/function/string-search/) — `Contains` / `StartsWith` / `EndsWith`, when the POSITION is not needed


---

<!-- https://osysharp.com/reference/function/text-join/ -->

# Text.Join

> Joins a list of values into one string, placing the separator between each pair. It is the inverse of Text.Split, and the same operation as string.Join. An empty list yields the empty string, and a single-element list yields just that element — the separator only ever falls BETWEEN elements. Runs in memory.

<!-- id: function-text-join · area: function · stability: stable · html: https://osysharp.com/reference/function/text-join/ -->

## Summary        {#summary}
`Text.Join(separator, values)` concatenates the elements of `values` into a single string, inserting
`separator` **between** each adjacent pair. It is the inverse of [Text.Split](https://osysharp.com/reference/function/text-split/), and identical to
`string.Join(separator, values)`.

## Signature      {#signature}
```osy syntax
Text.Join(<string> separator, <list> values) -> string
string.Join(<string> separator, <list> values) -> string   // the same operation
```

## Description    {#description}
The separator goes **only between** elements, never at the ends: joining `["a", "b", "c"]` with `", "` gives
`"a, b, c"` — two separators for three elements. Two consequences follow directly:

- an **empty** list joins to `""` (no elements, so no separators);
- a **single-element** list joins to just that element (nothing to put a separator between).

An **empty separator** simply concatenates: `Text.Join("", ["a", "b"])` is `"ab"`.

`Text.Join` and `Text.Split` are exact inverses when the separator does not itself appear inside an element:
splitting on `", "` and re-joining on `", "` returns the original string. `Text.Join` runs **in memory** — it
builds one string from a set of values already in hand.

## Examples       {#examples}
```osy title="re-join a split string — Join is Split's inverse" test app=text-join
// Split a CSV line, drop the empties, and re-join with a cleaner separator.
string Reflow(string csv) {
  return Text.Join(" · ", Text.Split(csv, ","));
}
```

```osy title="the exact answers, pinned" run app=text-join
[Test]
void Text_join_answers() {
  Assert.Equal("a · b · c", Reflow("a,b,c"));

  // Round-trip: split then join on the same separator returns the original.
  Assert.Equal("one, two, three", Text.Join(", ", Text.Split("one, two, three", ", ")));

  // A single element gets no separator; an empty separator just concatenates.
  Assert.Equal("solo", Text.Join(", ", Text.Split("solo", ",")));
  Assert.Equal("ab", Text.Join("", Text.Split("a,b", ",")));
}
```

## See also       {#see-also}
- [Text.Split](https://osysharp.com/reference/function/text-split/) — the inverse: string → list
- [String interpolation & format specifiers](https://osysharp.com/reference/function/string-interpolation/) — `$"{a}-{b}"`, the other way to build a string from values
- [Text.Length, Text.IsEmpty, Text.IsBlank, Text.Contains, Text.StartsWith, Text.EndsWith](https://osysharp.com/reference/function/text-inspect/) — asking questions about the resulting string


---

<!-- https://osysharp.com/reference/function/text-lastindexof/ -->

# Text.LastIndexOf

> The C# string.LastIndexOf: the index of the LAST ordinal occurrence of a substring, or -1 if absent. The 3-arg overload starts the backward search at startIndex (searching toward the beginning). Runs in memory only — there is no SQL push-down form.

<!-- id: function-text-lastindexof · area: function · stability: stable · html: https://osysharp.com/reference/function/text-lastindexof/ -->

## Summary        {#summary}
`Text.LastIndexOf(s, sub)` returns the zero-based index of the **last** ordinal occurrence of `sub` in
`s`, or `-1` when `sub` does not occur — exactly C#'s `string.LastIndexOf`. The 3-arg overload
`Text.LastIndexOf(s, sub, startIndex)` begins the search at `startIndex` and proceeds **backward toward
the beginning**, matching C#'s `LastIndexOf(value, startIndex)`.

## Signature      {#signature}
```osy syntax
Text.LastIndexOf(<string> s, <string> sub) -> int
Text.LastIndexOf(<string> s, <string> sub, <int> startIndex) -> int
```

## Description    {#description}
Matching is **ordinal** (byte-for-byte, culture-independent), the same as its forward-search sibling
[Text.IndexOf](https://osysharp.com/reference/function/text-indexof/). A miss returns `-1`.

⚠ **For word-boundary truncation, reach for [Text.Truncate](https://osysharp.com/reference/function/text-truncate/) instead.** It is the same idea done
once and correctly — it bounds the result by the budget *including* the ellipsis, handles a single long
word and a leading space, and trims the trailing space. Hand-rolled versions of it (including the `Clip`
below) routinely overshoot the budget they were given. Use `Text.LastIndexOf` directly when you want the
INDEX for something else.

`Text.LastIndexOf` is **in-memory only** — like `Text.Reverse`, it has no faithful SQL form, so it may be
called on locals inside a function body but not pushed down into a query predicate.

The 3-arg `startIndex` is the C# contract: it is the *last* position considered, and the search runs
backward. As in C#, an out-of-range `startIndex` faults rather than clamping.

## Examples       {#examples}
```osy title="finding the index itself" test app=text-search
string Clip(string content, int budget) {
  var cut = Text.LastIndexOf(content, " ", budget);   // last space at/before the budget
  if (cut < 0) {
    return content;                                    // no space → keep whole
  }
  return Text.Substring(content, 0, cut);
}
// Clip("the quick brown fox", 12)  ->  "the quick"
```

## See also       {#see-also}
- [Text.IndexOf](https://osysharp.com/reference/function/text-indexof/) — the forward-search sibling, with the same `startIndex` contract


---

<!-- https://osysharp.com/reference/function/text-inspect/ -->

# Text.Length, Text.IsEmpty, Text.IsBlank, Text.Contains, Text.StartsWith, Text.EndsWith

> Ask a string a question without changing it: its length, whether it is empty or blank, and whether it contains, starts with, or ends with a piece of text. The membership checks are ORDINAL and case-sensitive, and they take a literal piece of text — not a wildcard pattern. All run in memory.

<!-- id: function-text-inspect · area: function · stability: stable · html: https://osysharp.com/reference/function/text-inspect/ -->

## Summary        {#summary}
These are the read-only questions about a string. `Text.Length(s)` is its length; `Text.IsEmpty(s)` and
`Text.IsBlank(s)` test for emptiness; and `Text.Contains(s, part)`, `Text.StartsWith(s, part)`,
`Text.EndsWith(s, part)` test a string against a **literal** piece of text. The membership checks are
**ordinal** — case-sensitive, and the argument is plain text, not a pattern.

## Signature      {#signature}
```osy syntax
Text.Length(<string> s) -> int
Text.IsEmpty(<string> s) -> bool          // length is 0
Text.IsBlank(<string> s) -> bool          // empty, or only whitespace
Text.Contains(<string> s, <string> part) -> bool
Text.StartsWith(<string> s, <string> part) -> bool
Text.EndsWith(<string> s, <string> part) -> bool
```

## Description    {#description}

### Length is UTF-16 code units, not visible characters   {#length}
`Text.Length` counts **UTF-16 code units** (the C# `string.Length`), so an astral character outside the
Basic Multilingual Plane — an emoji, for instance — counts as **two**. `Text.Length("🎉")` is `2`, not `1`.
For plain text this is the character count you expect; the distinction only shows up on emoji and other
astral symbols.

### Empty vs blank   {#empty-vs-blank}
`Text.IsEmpty(s)` is true only for the zero-length string `""`. `Text.IsBlank(s)` is broader: it is true
for `""` **and** for a string that is entirely whitespace, using the full Unicode whitespace set — so an
ideographic space (`　`) or a no-break space (` `) counts as blank too, not just an ASCII space.

### Contains / StartsWith / EndsWith are ORDINAL and take a literal   {#ordinal-literal}
The match is **case-sensitive**: `Text.Contains("Hello World", "world")` is `false`. And the argument is a
**literal** run of text — a `%` or `_` in it is just that character, with no wildcard meaning. An empty
`part` is contained by everything: `Text.Contains(s, "")` is always `true`.

> **`Text.Contains(s, part)` and the member form `s.Contains(part)` are the same call.** The member spelling
> ([Contains, StartsWith, EndsWith](https://osysharp.com/reference/function/string-search/)) is just instance sugar — same ordinal, case-sensitive, literal semantics. Write
> whichever reads better. Both push into a query where the receiver is a column. For pattern matching on an in-hand
> string use [Regex](https://osysharp.com/reference/stdlib/regex/) — but only in memory; it has no query-predicate form.

All six run wherever they are needed — computed on a string already in hand, and (being pure) they run in the
browser with no round trip where a UI action needs them, or push into a query when the receiver is a column
([execution side](https://osysharp.com/reference/function/execution-side/)).

## Examples       {#examples}
```osy title="a field validator built from the inspection functions" test app=text-inspect
// Reject a blank required field, and flag anything past a length budget.
string CheckName(string name) {
  if (Text.IsBlank(name)) { return "required"; }
  if (Text.Length(name) > 40) { return "too long"; }
  return "ok";
}
```

```osy title="the exact answers, pinned" run app=text-inspect
[Test]
void Text_inspection_answers() {
  Assert.Equal("required", CheckName("   "));    // whitespace-only fails the blank check
  Assert.Equal("ok", CheckName("Ada Lovelace"));


  Assert.Equal(5, Text.Length("hello"));
  Assert.Equal(2, Text.Length("🎉"));            // UTF-16 code units — the emoji counts twice

  Assert.True(Text.IsEmpty(""));
  Assert.False(Text.IsEmpty(" "));               // a space is not empty…
  Assert.True(Text.IsBlank("   "));              // …but it is blank
  Assert.False(Text.IsBlank(" hi "));

  Assert.True(Text.Contains("Hello World", "World"));
  Assert.False(Text.Contains("Hello World", "world"));   // ordinal → case-sensitive
  Assert.True(Text.Contains("Hello", ""));               // everything contains the empty string
  Assert.True(Text.StartsWith("Hello", "He"));
  Assert.False(Text.StartsWith("Hello", "he"));
  Assert.True(Text.EndsWith("Hello", "lo"));
}
```

## See also       {#see-also}
- [Contains, StartsWith, EndsWith](https://osysharp.com/reference/function/string-search/) — the member-call spelling `s.Contains(x)`, the same ordinal-literal test as this
- [Regex](https://osysharp.com/reference/stdlib/regex/) — pattern matching, when a literal check is not enough
- [Text.Split](https://osysharp.com/reference/function/text-split/) · [Text.TitleCase](https://osysharp.com/reference/function/text-titlecase/) — the other in-memory string builtins


---

<!-- https://osysharp.com/reference/function/text-like/ -->

# Text.Like

> Matches a string against a wildcard pattern, where % stands for any run of characters and _ for exactly one. It is the only wildcard search in the language — the instance methods Contains, StartsWith and EndsWith are literal — and it is the one that a database can answer with an index. It gives the same answer in the browser, in a function body, and pushed down into a query.

<!-- id: function-text-like · area: function · stability: stable · html: https://osysharp.com/reference/function/text-like/ -->

## Summary        {#summary}
`Text.Like(s, pattern)` tests a string against a **wildcard pattern**. It is the deliberate opposite of
[Contains, StartsWith, EndsWith](https://osysharp.com/reference/function/string-search/): there the argument is data, here it is a pattern.

## Signature      {#signature}
```osy syntax
Text.Like(<string>, <string>) -> bool
```

## Description    {#description}

### The pattern   {#pattern}

| in the pattern | matches |
|---|---|
| `%` | any run of characters, including none |
| `_` | exactly one character |
| `\%` `\_` `\\` | a literal `%`, `_` or `\` |
| anything else | itself |

```osy title="what each wildcard matches, and escaping a literal percent" syntax
Text.Like(code, "RUSH-%")        // begins with RUSH-
Text.Like(code, "%-2026")        // ends with -2026
Text.Like(code, "A_-%")          // A, then any one character, then "-", then anything
Text.Like(label, @"50\% off")    // a LITERAL percent sign
```

### Why it is not a method on the string   {#vs-contains}

`s.Contains(x)`, `s.StartsWith(x)` and `s.EndsWith(x)` are **literal** searches — a `%` in the argument is a percent
sign. `Text.Like` is the wildcard one. They are spelled differently on purpose: the difference between "find this text"
and "find things shaped like this" should be visible where you read the call, not something you have to remember. C#
has no `Like` at all, and EF Core makes the same split for the same reason (`EF.Functions.Like`, never a method on
`string`).

### It is case-sensitive   {#case}

`Text.Like("ACME Ltd", "acme%")` is **false**. To ignore case, lower both sides:

```osy title="case-sensitive too — lower both sides" syntax
Text.Like(c.Name.ToLower(), "acme%")
```

### Inside a query, it can use an index   {#index}

This is the practical reason it exists. `Contains` becomes a substring search that has to look at every row; a `Like`
whose pattern is anchored at the front (`"RUSH-%"`) can be answered from a btree index on the column. A pattern that
starts with `%` cannot — it has nothing to seek to — so prefer an anchored pattern when the table is large.

### A pattern that ends with a bare `\`   {#trailing-escape}

A trailing escape character escapes nothing, so such a pattern can never match anything. When the pattern is written
out in the source it is a **compile error**; double it (`\\`) if you meant a literal backslash.

### One answer, wherever it runs   {#same-everywhere}

The same call gives the same result in a browser action, in a function body on the server, and compiled into SQL — the
three implementations are tested against each other over a corpus that includes `%`, `_`, `\`, newlines and characters
outside the Basic Multilingual Plane.

## Examples       {#examples}
```osy title="find codes by shape" test app=text-like
entity Product { string Code; string Name; }

List<Product> RushCodes() {
  return Product.Where(p => Text.Like(p.Code, "RUSH-%")).ToList();   // anchored: an index can serve it
}

bool LooksLikeABatch(string code) {
  return Text.Like(code, "B__-____");        // B, two characters, a dash, four characters
}

bool MentionsAPercentage(string label) {
  return Text.Like(label, @"%\%%");          // contains a literal percent sign
}
```

## See also       {#see-also}
- [Contains, StartsWith, EndsWith](https://osysharp.com/reference/function/string-search/) — `Contains`/`StartsWith`/`EndsWith`, the LITERAL searches
- [Regex](https://osysharp.com/reference/stdlib/regex/) — full regular expressions, in memory only (no query form)
- [execution side](https://osysharp.com/reference/function/execution-side/) — why this runs in the browser too


---

<!-- https://osysharp.com/reference/function/text-split/ -->

# Text.Split

> The C# string.Split: breaks a string on a separator and returns the substrings as a List<string> — iterable with foreach and queryable with .Count / .Contains. Empty segments are kept, exactly like C#'s StringSplitOptions.None. Runs in memory only.

<!-- id: function-text-split · area: function · stability: stable · html: https://osysharp.com/reference/function/text-split/ -->

## Summary        {#summary}
`Text.Split(s, separator)` splits `s` on each occurrence of `separator` and returns the substrings as a
**`List<string>`** — the mutable-list shape, so the result is iterable with `foreach` and supports
`.Count` and `.Contains`. This is the inverse of `string.Join`, and mirrors C#'s
`string.Split(separator)` with `StringSplitOptions.None`: **empty segments are kept**.

## Signature      {#signature}
```osy syntax
Text.Split(<string> s, <string> separator) -> List<string>
```

## Description    {#description}
The result is a real `List<string>` (not a read-only query result), so the collection surface applies:
`foreach`, `.Count`, `.Contains`, and passing it to `string.Join`. Membership and iteration are the
idiomatic ways to consume it.

Empty handling is **C#-faithful (`StringSplitOptions.None`)**: `Text.Split("a,,b", ",")` yields three
elements `["a", "", "b"]`, and `Text.Split("", ",")` yields a single empty element `[""]`.

`Text.Split` is **in-memory only** — a split produces a set, which has no SQL push-down form, so it is
called on locals inside a function body, never inside a query predicate.

## Examples       {#examples}
```osy title="trim each CSV field" test app=text-search
List<string> TrimFields(string csv) {
  var trimmed = new List<string>();
  foreach (var field in Text.Split(csv, ",")) {
    trimmed.Add(Text.Trim(field));
  }
  return trimmed;
}
// TrimFields("orders, customers , items")  ->  ["orders", "customers", "items"]
```

## See also       {#see-also}
- [String interpolation & format specifiers](https://osysharp.com/reference/function/string-interpolation/) — `string.Join` is the inverse (list → string)
- [Text.LastIndexOf](https://osysharp.com/reference/function/text-lastindexof/) — the other in-memory-only string builtin


---

<!-- https://osysharp.com/reference/function/text-titlecase/ -->

# Text.TitleCase

> Capitalises the first letter of each word and lower-cases the rest, leaving a word that is already entirely upper-case untouched so acronyms survive. Word breaks fall on any non-letter except the apostrophe, so "o'brien" becomes "O'brien" but "mcdonald-smith" becomes "Mcdonald-Smith".

<!-- id: function-text-titlecase · area: function · stability: stable · html: https://osysharp.com/reference/function/text-titlecase/ -->

## Summary        {#summary}
`Text.TitleCase(s)` capitalises the first letter of each word in `s` and lower-cases the rest — except that a word
which is **already entirely upper-case is left exactly as it is**, so an acronym is not quietly mangled into
`Nasa`.

## Signature      {#signature}
```osy syntax
Text.TitleCase(<string> s) -> string
```

## Description    {#description}
A **word** is a run of letters, optionally containing an apostrophe. Every other character — a space, a hyphen, a
digit, a full stop — ends the word and starts a new one. That has two consequences worth knowing before you use it
on names:

| Input | Result | Why |
|---|---|---|
| `"hello world"` | `"Hello World"` | the ordinary case |
| `"NASA report"` | `"NASA report"` → `"NASA Report"` | an all-caps word is preserved |
| `"hELLO"` | `"Hello"` | a mixed-case word has its tail lower-cased |
| `"o'brien"` | `"O'brien"` | an apostrophe does **not** break a word |
| `"mcdonald-smith"` | `"Mcdonald-Smith"` | a hyphen **does** |
| `"3rd place"` | `"3Rd Place"` | a digit is not a letter, so `rd` begins a fresh word |

The last two rows are the ones that surprise people. `Text.TitleCase` is a mechanical transformation, not a
name-formatter: if you need `McDonald` or `3rd`, write the casing you want rather than deriving it.

Casing is **invariant** — it does not depend on the machine's locale, so the same input gives the same output
everywhere, and it gives the same output whether the function runs in the browser or on the server.

## Examples       {#examples}
```osy title="tidy up a user-entered display name" test app=text-search
string DisplayName(string raw) {
  return Text.TitleCase(Text.Trim(raw));
}
// DisplayName("  ada LOVELACE ")  ->  "Ada LOVELACE"
```

## See also       {#see-also}
- [Text.Split](https://osysharp.com/reference/function/text-split/) — the other in-memory string builtins
- [execution side](https://osysharp.com/reference/function/execution-side/) — why this runs in the browser, with no round trip


---

<!-- https://osysharp.com/reference/function/text-affix/ -->

# Text.TrimStart, Text.TrimEnd, Text.PadStart, Text.PadEnd

> Trim whitespace from one end of a string, or pad it out to a width with a fill string. Trimming uses the full Unicode whitespace set. Padding never truncates — a string already at or over the width comes back unchanged — and the fill is a STRING, cut to fit the gap exactly. All run in memory.

<!-- id: function-text-affix · area: function · stability: stable · html: https://osysharp.com/reference/function/text-affix/ -->

## Summary        {#summary}
`Text.TrimStart(s)` and `Text.TrimEnd(s)` remove whitespace from one end of a string (`Text.Trim(s)` does
both). Give any of the three a **second argument** and it removes those CHARACTERS instead — C#'s
`TrimEnd(params char[])`, with the set written as a string because Osy# has no `char` type. `Text.PadStart(s, width, pad)` and `Text.PadEnd(s, width, pad)` grow a string to at least `width`
characters by adding a fill on the left or right. Padding **never shortens** a string, and the fill is a
**string**, not a single character.

## Signature      {#signature}
```osy syntax
Text.TrimStart(<string> s) -> string          // strip leading whitespace
Text.TrimEnd(<string> s)   -> string          // strip trailing whitespace
Text.TrimStart(<string> s, <string> chars) -> string   // …or strip any of THESE characters
Text.TrimEnd(<string> s, <string> chars)   -> string
Text.Trim(<string> s, <string> chars)      -> string   // both ends
Text.PadStart(<string> s, <int> width, <string> pad) -> string   // fill on the LEFT to `width`
Text.PadEnd(<string> s, <int> width, <string> pad)   -> string   // fill on the RIGHT to `width`
```

## Description    {#description}

### Trimming characters rather than whitespace   {#chars}
The second argument is a SET, not a suffix: every character in it is stripped from that end, repeatedly, exactly as
C#'s `char[]` overload does. So `TrimEnd("/")` removes as many trailing slashes as there are, and `TrimEnd("/\\")`
removes either kind.

```osy title="the path-joining line every app writes" test app=function-text-affix-chars
string JoinUrl(string baseUrl, string path) {
  return Text.TrimEnd(baseUrl, "/") + "/" + Text.TrimStart(path, "/");
}
```

⚠ It is a set of characters, so `TrimEnd(url, "/api")` strips any trailing `/`, `a`, `p` or `i` — not the word
"api". That is C#'s behaviour too, and it is the one thing about these overloads that surprises people.

### Trimming uses the full Unicode whitespace set   {#trimming}
`TrimStart`/`TrimEnd` strip every leading/trailing whitespace character, not just the ASCII space — a tab, a
newline, a no-break space (` `), an ideographic space (`　`) are all removed. Only the named end is touched:
`Text.TrimStart("  hi  ")` is `"hi  "` (trailing spaces survive).

### Padding never truncates, and takes a WIDTH not a count-to-add   {#padding-width}
`width` is the **target length**, not "how many characters to add". If the string is already at least that
long it comes back **unchanged**: `Text.PadStart("hello", 3, "0")` is `"hello"`. Otherwise the gap is filled
to reach exactly `width`.

### The pad is a STRING, cut to fit the gap   {#pad-string}
The fill is repeated and then **cut to exactly the deficit**, so a multi-character pad can end mid-repeat:
`Text.PadStart("7", 3, "ab")` fills two characters — `"ab7"` — and `Text.PadEnd("7", 4, "ab")` is `"7aba"`.
An **empty** pad is a no-op (it cannot fill anything), so `Text.PadStart("x", 5, "")` is `"x"` — it does not
loop forever. For zero-padding a *number*, a format specifier like `D5` or `"00000"` is usually clearer than
padding a string — see [format specifiers](https://osysharp.com/reference/function/format-specifiers/).

All four run **in memory** on a value already in hand.

## Examples       {#examples}
```osy title="normalise then right-justify an amount" test app=text-affix
// Trim stray whitespace, then right-justify into a fixed-width column with leading zeros.
string Ticket(string code) {
  return Text.PadStart(Text.TrimEnd(Text.TrimStart(code)), 6, "0");
}
```

```osy title="the exact answers, pinned" run app=text-affix
[Test]
void Text_affix_answers() {
  Assert.Equal("000042", Ticket("  42  "));

  Assert.Equal("hi  ", Text.TrimStart("  hi  "));   // only the leading end
  Assert.Equal("  hi", Text.TrimEnd("  hi  "));
  Assert.Equal("hi", Text.TrimStart("　hi"));        // an ideographic space is whitespace too

  Assert.Equal("007", Text.PadStart("7", 3, "0"));
  Assert.Equal("ab7", Text.PadStart("7", 3, "ab"));  // multi-char pad, cut to the 2-char gap
  Assert.Equal("hello", Text.PadStart("hello", 3, "0"));   // already long enough → unchanged
  Assert.Equal("x", Text.PadStart("x", 5, ""));      // empty pad → no-op, never an infinite loop
  Assert.Equal("700", Text.PadEnd("7", 3, "0"));
  Assert.Equal("7aba", Text.PadEnd("7", 4, "ab"));
}
```

## See also       {#see-also}
- [Text.Length, Text.IsEmpty, Text.IsBlank, Text.Contains, Text.StartsWith, Text.EndsWith](https://osysharp.com/reference/function/text-inspect/) — `Text.Length`, and the emptiness checks
- [format specifiers](https://osysharp.com/reference/function/format-specifiers/) — zero-padding a number (`D5`, `"00000"`), usually better than padding a string
- [Text.TitleCase](https://osysharp.com/reference/function/text-titlecase/) — the other in-memory string builtins


---

<!-- https://osysharp.com/reference/function/text-truncate/ -->

# Text.Truncate

> Shorten a string to at most maxLength characters — INCLUDING the ellipsis — cutting back to the last word boundary rather than mid-word. The bound covers the ellipsis on purpose: a caller truncating to a budget can add the result to its total without re-checking it.

<!-- id: function-text-truncate · area: function · stability: stable · html: https://osysharp.com/reference/function/text-truncate/ -->

## Summary        {#summary}
`Text.Truncate(s, maxLength)` returns `s` unchanged when it already fits, and otherwise shortens it to
**at most `maxLength` characters, ellipsis included**, backing off to the last word boundary so a word is
never cut in half.

## Signature      {#signature}
```osy syntax
Text.Truncate(<string> s, <int> maxLength) -> string
```

## Description    {#description}
The length bound **includes the ellipsis** (`…`, a single character). That is the point of the method: the
usual reason to truncate is that you are spending a budget — a token budget in a prompt, a column width in
a table — and a helper that can overshoot its own limit forces the caller to measure the result again.

Behaviour at the edges, each of which a hand-rolled version tends to get wrong:

- **A single long word** has no boundary to back off to, so it is cut hard: `Text.Truncate("supercalifragilistic", 10)`
  is `"supercali…"`, not `""`.
- **A leading space** does not empty the string — the back-off only applies to a boundary found *past* the
  start.
- **`maxLength` of 1** leaves room for the ellipsis alone; **0 or less** returns an empty string rather than
  faulting.

Trailing whitespace is trimmed before the ellipsis is appended, so you never get `"the quick …"`.

`Text.Truncate` runs **in memory** — call it on locals inside a function body, not inside a query
predicate.

## Examples       {#examples}
```osy title="assembling context under a budget" test app=text-search
string Excerpt(string body, int budget) {
  return Text.Truncate(body, budget);
}
// Excerpt("the quick brown fox jumps", 12)  ->  "the quick…"   (10 chars — inside the budget)
// Excerpt("hello", 20)                      ->  "hello"        (already fits, untouched)
```

Because the result is bounded, a budget loop can trust it:

```osy title="a budget loop that can trust the bounded result" syntax
var line = "- " + Text.Truncate(item.Content, 200) + "\n";
var cost = Text.Length(line) / 4;
if (used + cost > budget) { break; }
```

## See also       {#see-also}
- [Text.LastIndexOf](https://osysharp.com/reference/function/text-lastindexof/) — the backward search `Text.Truncate` is built on; reach for it directly
  only when you need the index itself rather than a shortened string
- [String interpolation & format specifiers](https://osysharp.com/reference/function/string-interpolation/) — building the strings you are truncating


---

<!-- https://osysharp.com/reference/function/tuples/ -->

# Tuples and deconstruction

> A function returns several values by declaring a tuple return type and returning a parenthesized list. The caller reads them by name (`t.ok`), or deconstructs them straight into locals (`var (ok, value) = …`). Tuples are how Osy# expresses the `TryParse` shape — there are no `out` parameters, because a call here can suspend and resume elsewhere, and only a return survives that.

<!-- id: function-tuples · area: function · stability: preview · html: https://osysharp.com/reference/function/tuples/ -->

## Summary        {#summary}
A function that has **two things to say** should not have to invent a type for them. Declare the return as a tuple:

```osy title="two values out of one call" test app=function-tuples
class Parser {
  public (bool ok, decimal value) Try(string s) {
    if (s == "1") { return (true, 1m); }
    return (false, 0m);
  }
}
```

The caller takes both:

```osy title="deconstruct at the call site" test app=function-tuples
decimal Read(string s) {
  var p = new Parser();
  var (ok, value) = p.Try(s);
  return ok ? value : 0m;
}
```

## Signature      {#signature}
```osy syntax
(T1 name1, T2 name2) Fn(…) { … }     // a tuple RETURN type; names are optional
return (expr1, expr2);               // a tuple LITERAL
var (a, b) = Fn(…);                  // DECONSTRUCTION into new locals
(T1 a, T2 b) t = Fn(…);              // …or a typed local holding the whole tuple
t.name1                              // an element by the name written on the type
t.Item1                              // …or by position, 1-based
```

A tuple has at least **two** elements. Element names are optional (`(bool, decimal)` is the same type) and may differ
between declarations.

## Description    {#description}

### Why not `out`   {#no-out}
C#'s `TryParse` shape writes its second value back **through a parameter**. Osy# has no `out` (nor `ref`) *to
declare*, and the reason is not stylistic: a call here can **suspend** and resume later, possibly on the other side
of the client/server boundary. A pending write-back into a caller's frame has no meaning once that frame may be gone.
A return survives by construction, so the value comes back as one.

```osy title="the shape `out` would have had" syntax
public bool Try(string s, out decimal v) { … }   // ✗ not supported — see the refusal, which names this page
public (bool ok, decimal value) Try(string s)    // ✓ the same information, as a return
```

⚠ **The built-in `T.TryParse` is the exception, and it is not one you have to remember.**
`decimal.TryParse(s, out var v)` — the CALL, not a declaration of your own — compiles: the compiler rewrites it
into `decimal? v = null; try { v = decimal.Parse(s); } catch { }` ahead of the statement. That is only available for
the stdlib parses; a function of your own that wants to hand back two values returns a tuple, as below.

### The identity is the element TYPES, not their names   {#identity}
Two tuples with the same element types are the **same type**, whatever their elements are called — C#'s rule, and
what keeps them interchangeable:

```osy title="one shape, two spellings, one type" test app=function-tuples
(bool ok, decimal value) First() { return (true, 1m); }
(bool success, decimal amount) Second() { return (false, 0m); }

decimal Either(bool pick) {
  var a = First();
  var b = Second();
  a = b;                        // the same type — names are labels, not identity
  return a.ok ? 1m : 0m;
}
```

Names are read at the place they are **written**: `a.ok` works because `a` came from a type spelled with `ok`.

### Reading the elements   {#reading}
By name, or by position. Both address the same slots, so a tuple never has "two ways to be right":

```osy title="by name and by position" test app=function-tuples
decimal Both(string s) {
  var p = new Parser();
  var t = p.Try(s);
  return t.ok == t.Item1 ? t.value : t.Item2;
}
```

### Deconstruction evaluates the call ONCE   {#deconstruction}
`var (ok, value) = p.Try(s)` reads the call a single time into a hidden local, then takes each element from it. That
matters whenever the call does something: writing `p.Try(s).Item1` and `p.Try(s).Item2` would run it **twice**.

## Examples       {#examples}

A parse that reports whether it succeeded, without a nullable and without a wrapper class:

```osy title="the TryParse shape, end to end" test app=function-tuples
class Amounts {
  public (bool ok, decimal value) Parse(string raw) {
    if (raw == "") { return (false, 0m); }
    return (true, 42m);
  }
}

decimal Total(string raw) {
  var a = new Amounts();
  var (parsed, amount) = a.Parse(raw);
  return parsed ? amount : 0m;
}
```

A tuple as a parameter, when a pair travels together:

```osy title="a tuple travelling as one value" test app=function-tuples
decimal Apply((bool ok, decimal value) result) {
  return result.ok ? result.value : 0m;
}
```

## Errors         {#errors}

| What you wrote | What you get |
|---|---|
| `(decimal) Fn(…)` | *a tuple needs at least TWO elements — `(bool ok, decimal value)`. A one-element tuple has nothing to hold that the type itself does not; write the type on its own.* |
| `var (only) = Fn();` | *a deconstruction takes at least TWO names — `var (ok, value) = …`. For one value write `var ok = …` without the parentheses.* |
| a literal whose shape nothing declares | *no tuple type `(decimal, decimal)` is declared in this app, so there is nothing for this literal to be. A tuple shape comes from a DECLARATION — write it as a return type … and the literal will match it.* |
| `t.valu` | *this tuple has no element 'valu'. Did you mean 'value'? — it holds `ok`, `value`* |
| `public bool Try(out string v)` | *`out` is not supported — a parameter passes a value IN, and only the RETURN comes back out … For the `TryParse` shape return a NULLABLE and test it, … or return a small `class`.* |

## See also       {#see-also}
- [Functions (the unit of work)](https://osysharp.com/reference/function/index/) — what a function is, and how it is called
- [Classes](https://osysharp.com/reference/class/index/) — the alternative when a pair wants a name and behaviour of its own
- [Generic classes](https://osysharp.com/reference/class/generics/) — the other way one declaration serves many types


---

<!-- https://osysharp.com/reference/function/typed-locals/ -->

# Typed locals

> Locals can declare an explicit type instead of var; the declared type pins the binding, and every later assignment to the name is checked against it by the same rule. Literal initializers apply the C# constant conversion; non-literals widen by numeric rank but never across decimal↔double.

<!-- id: function-typed-locals · area: function · stability: stable · html: https://osysharp.com/reference/function/typed-locals/ -->

## Summary        {#summary}
Locals can declare an explicit type instead of `var` — `int x = 5;`, `Order o = …;`,
`List<int> xs = …;`. The declared type **pins** the binding, exactly C#: literal initializers apply the
C# constant conversion, non-literal initializers must be implicitly assignable, and every typed local
must be initialized at its declaration. **The same rule governs every later `x = …`** — the declaration
and the assignment on the next line are checked by one function, so they cannot disagree.

## Signature      {#signature}
```osy syntax
<Type> <name> = <initializer>;
<name> = <value>;                     // …and every later assignment, by the same rule
// Type: a scalar keyword, entity (incl. namespaced), Type?, Type[], Type[][],
//       List<T>/HashSet<T>/Dictionary<K,V>
```

## Description    {#description}
- **A typed local must be initialized at its declaration** — `int x;` is refused (there is no
  definite-assignment analysis; initialize where you declare).
- **Literal initializers use the C# constant conversion**: the literal is re-kinded when widening
  (`int → long/decimal/double`, `decimal → double`) — so `double h = 2.5;` compiles even though a bare
  `2.5` is decimal ([Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/)). A non-representable constant refuses:
  `int x = 2.5m;` → `cannot implicitly convert 'decimal' to 'int'`.
- **Non-literal initializers widen by numeric rank** (`int → long → decimal/double`) but **never across
  decimal↔double** in either direction — C# has no implicit conversion between them
  (`double x = someDecimal;` is a pointed error).
- **`null` needs a nullable declared type** — `int? x = null;`, not `int x = null;`.
- The pinned type drives downstream resolution — an entity-typed local navigates members
  (`Order o = Order.First(); o.Total`).
- **Decompile normalizes to `var`** with the re-kinded literal (`decimal d = 5;` → `var d = 5m;`) —
  semantically identical; the same normalization const-inlining uses.

### Assigning to it afterwards    {#assignment}
**Every assignment is checked against the slot, by the rule above.** `int x = 1; x = "hello";` is refused, and the
message names the variable, what it holds, what you tried to store and the line the type was decided on. This holds
wherever the value lands and however it is spelled:

| The slot | Example |
|---|---|
| a typed local, or one inferred by `var` | `int x = 1; x = "hello";` |
| a parameter | `void F(int p) { p = "hello"; }` |
| a `for` / `foreach` / `catch` variable | `foreach (var i in xs) { i = "hello"; }` |
| a compound assignment | `x += "hello";` |
| a chain | `a = b = "hello";` |
| a component field, with or without `this.` | `counter = "hello";` inside an `action` or a render lambda |
| a dictionary, list or array element — and the key | `d[42] = 1;` on a `Dictionary<string, int>` |
| a class indexer's `set` | `bag["a"] = "hello";` on `int this[string key]` |

The conversions it ACCEPTS are the same ones a declaration, an argument and a `return` accept: the C# constant
conversion for a numeric literal (`decimal d = 0; d = 1;` stores a decimal), numeric widening by rank, and the
implicit upcast to a base type. `decimal ↔ double` is refused in both directions, as in C#.

## Examples       {#examples}
```osy title="typed locals" test app=typed-locals
entity Order { decimal Total; }

decimal Examples() {
  int x = 5;                         // pins int
  long big = 5;                      // constant conversion: the int constant becomes long
  decimal d = 5;                     // → 5m
  double h = 2.5;                    // works — the constant converts (a bare 2.5 is decimal)
  int? maybe = null;                 // nullable declared type accepts null
  Order? o = Order.FirstOrDefault(); // entity-typed local — `?`, because …OrDefault() may answer null
  if (o != null) { return o.Total + d; }
  return d + x + big + maybe ?? 0;
}
```

```osy title="…and the same rule on every assignment after it" test app=typed-locals
decimal Later() {
  int x = 1;
  x = 2;                             // fine — same type
  decimal d = 0;
  d = 1;                             // fine — the int constant becomes a decimal, as at a declaration
  long big = 0;
  big = x;                           // fine — int widens to long
  // x = "hello";                    // refused: cannot assign 'string' to `x`, which holds 'int'
  // d = 1.5;   ⟵ fine too; but `double h = 0; h = d;` is refused — no decimal↔double conversion
  return d + big;
}
```

⚠ **The `?` on `o` is the example, not a typo.** A `…OrDefault()` read answers null when nothing matches, so a
non-nullable `Order o` would be holding a null the moment the table is empty — and every later read of it would be an
unguarded one. The compiler refuses that declaration and names both honest choices: `Order? o` if absent is a case you
handle, or `First()` if it is not, which fails loudly at the read instead of handing you a null that surfaces
somewhere else. This page carried the non-nullable form until 2026-08-27, when the check that catches it landed.

## See also       {#see-also}
- [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) — the literal kinds the constant conversion re-kinds between
- [const](https://osysharp.com/reference/function/const/) — `const Type name = …;` (the compile-time-constant sibling)


---

<!-- https://osysharp.com/reference/function/unit-of-work/ -->

# UnitOfWork

> Every body accumulates its writes in a unit of work rather than sending them one at a time. UnitOfWork.Commit() makes everything accumulated so far durable, atomically — all of it lands or none of it does, and UnitOfWork.Discard() throws that accumulation away instead. Reads inside the same body already see the pending writes, so nothing needs saving before it can be used.

<!-- id: function-unit-of-work · area: function · stability: stable · html: https://osysharp.com/reference/function/unit-of-work/ -->

## Summary        {#summary}

Every body accumulates its writes in a **unit of work** rather than sending them one at a time.
`UnitOfWork.Commit()` makes everything accumulated so far durable, **atomically** — all of it lands or none of it
does — and `UnitOfWork.Discard()` throws that accumulation away instead. Reads inside the same body already see the
pending writes, so nothing needs saving before it can be used.

## Signature      {#signature}

```osy syntax
UnitOfWork.Commit()     // persist everything accumulated, atomically
UnitOfWork.Discard()    // drop everything accumulated, in THIS unit of work only
```

## Description    {#description}

### What a unit of work is   {#what}

Writing `new Order { … }`, assigning a property, or calling `.Delete()` does not talk to the database. It records
the change in the unit of work that the current body is running inside. `UnitOfWork.Commit()` is what sends the
accumulated changes, and it sends them as one atomic act: if any part fails — an invariant, a constraint, a
security rule — **nothing** is written.

That is the reason the verb names the unit of work rather than the row. There is no per-row save, because a save is
never about one row: it is about everything the body has done so far.

### Reads already see pending writes   {#read-your-writes}

A row you have created or modified is visible to the rest of the body immediately, including through queries.
This is the property that makes a body readable — you write what you mean in the order you mean it, and the last
line makes it durable:

```osy title="a query sees a row that has not been committed yet" test app=function-unit-of-work
entity Invoice {
  [Required, MaxLength(20)] string Code;
  decimal Total;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

decimal AddAndTotal(string code, decimal amount) {
  var inv = new Invoice { Code = code, Total = amount };

  // Not committed yet — and already found by an ordinary query, because the query
  // reads the unit of work's view of the data, not the database's.
  var sum = Invoice.Sum(i => i.Total);

  UnitOfWork.Commit();
  return sum;
}
```

### Committing is not automatic, and nothing warns you at run time   {#commit-required}

A body that writes and never commits loses the write silently. There is no exception and no log line: the change
was recorded in a unit of work that was then discarded. In a UI this is especially convincing, because the screen
updates from the pending write and *looks* saved.

The platform therefore catches it at **compile** time instead. `data-write-never-committed` is a MUST-tier lint
finding that names the verb that writes, and it stays quiet as soon as something in that flow commits — so the two
correct shapes below both satisfy it.

### The two correct shapes   {#shapes}

**Commit per act.** Ticking a to-do *is* the save; each verb is a complete act and commits for itself.

```osy title="each call is a complete act" test app=function-unit-of-work
entity Todo {
  [Required, MaxLength(120)] string Title;
  bool IsDone;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

void ToggleTodo(Todo t) {
  t.IsDone = !t.IsDone;
  UnitOfWork.Commit();
}
```

**Commit once, at the end.** A form accumulates freely and commits when the user saves. Every write between the
first and the commit is part of the same atomic act — which is what makes a half-saved form impossible.

```osy title="many writes, one atomic commit" test app=function-unit-of-work
entity Customer {
  [Required, MaxLength(120)] string Name;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

entity Address {
  [Required] Customer Owner;
  [Required, MaxLength(200)] string Line1;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

void Register(string name, string line1) {
  var c = new Customer { Name = name };
  var a = new Address { Owner = c, Line1 = line1 };
  UnitOfWork.Commit();      // both rows, or neither
}
```

### Throwing the accumulation away — `UnitOfWork.Discard()`   {#discard}

`Discard()` is the other half of the same decision: it drops everything the unit of work has accumulated instead of
persisting it. The rows return to what the server last confirmed, and the body carries on.

```osy title="abandon the pending edits without abandoning the body" test app=function-unit-of-work
entity Draft {
  [Required, MaxLength(200)] string Body;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

void StartOver(Draft d, string replacement) {
  d.Body = "";
  UnitOfWork.Discard();          // that edit is gone
  d.Body = replacement;          // this one is not — the unit of work continues
  UnitOfWork.Commit();
}
```

A row the discarded unit of work **created** is a different matter from one it merely edited. The edit above has
something to fall back to — the value the server last confirmed — but a row that only ever existed in the discarded
work has nothing: it never reached the database and never will. Reading or writing one is refused by name rather
than answered with `null` or staged where nothing will commit it:

```osy title="a row the discard threw away has nothing to fall back to" syntax
var d = new Draft { Body = "…" };
UnitOfWork.Discard();
d.Body = "changed";     // refused: the Draft row … was created and then DISCARDED
```

*"…everything this unit of work had staged was thrown away — by an explicit `UnitOfWork.Discard()`, or by the
cleanup that follows a `Throws`/`Denied` assert catching a commit fault."* Create it **after** the discard, or keep
the work you want out of the unit of work you are about to throw away.

**`Commit` and `Discard` are deliberately not symmetric, and the asymmetry is load-bearing.** A commit reaches
OUTWARD — persisting is the outermost unit of work's job, so an inner scope's edits have to reach it. A discard
clears exactly ONE unit of work, the one you are in, and stops. If it reached outward too, closing an inner surface
would take the surrounding page's unsaved work with it.

### Failure leaves nothing behind   {#failure}

If a commit is refused, the writes it carried are gone — including the ones that were individually valid. A body
that wants to record something about the failure must do that work **after** catching it, and commit again; see
[try / catch / finally](https://osysharp.com/reference/function/try-catch/) for the worked example.

### Where it runs   {#execution-side}

`UnitOfWork.Commit()` is a server act. Called from a UI action it hands off to the server, persists, and returns —
the client's pending edits become durable at that moment. Nothing about the spelling changes between a server
function and a UI action, which is the point: the same sentence means the same thing in both. See
[execution side](https://osysharp.com/reference/function/execution-side/) for how a body is split, and [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) for the UI-facing story of building
a form around it.

## See also       {#see-also}
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — creating and saving data from a UI action, and choosing between the two shapes above
- [entity](https://osysharp.com/reference/entity/declaration/) — the entities a unit of work persists
- [try / catch / finally](https://osysharp.com/reference/function/try-catch/) — what survives a refused commit, and how to record the failure
- [execution side](https://osysharp.com/reference/function/execution-side/) — why a commit is a server act even when written in a client body


---

<!-- https://osysharp.com/reference/function/async-await/ -->

# async / await — why Osy# has neither

> Osy# has no async and no Task. A function that calls out to the world is written like any other function — the engine suspends and resumes it around the effect. await exists in exactly one place, Workflow.Run.

<!-- id: function-async-await · area: function · stability: stable · html: https://osysharp.com/reference/function/async-await/ -->

## Summary        {#summary}
**There is no `async` in Osy#, and no `Task<T>`.** A function that calls out to the world — an HTTP request, an LLM
completion — is written exactly like one that adds two numbers. You call the thing; the platform takes care of the
waiting.

`await` exists in **one** place in the whole language: `await Workflow.Run("step", …)`.

If you are coming from C#, this is the first thing to unlearn, so it is worth a page of its own.

## Signature      {#signature}
```osy syntax
// No `async`. No `Task<T>`. No `.Result`, no `ConfigureAwait`, no `Task.WhenAll`.
<ReturnType> <Name>(<params>) {
  var r = Http.Get(url);      // an effect — just call it
  …
}

await Workflow.Run("step", <workflow>);   // the ONE await: wait for another long-running thing to finish
```

## Description    {#description}

### Just call the effect   {#just-call}
An **effect** is anything that reaches outside the database: an HTTP call, a model completion, a file read. You call
it. That is all:

```osy title="calling out, with no ceremony" test app=function-async-await
app Shop {
  model "model/**/*.osy";
  use Osysharp.Http;
}

entity Order {
  [Required] string Code;
  bool Notified;
}

void Notify(string code, string url) {
  var order = Order.Single(o => o.Code == code);
  var r = Http.Get(url);         // the function pauses here — but you did not have to say so
  order.Notified = r.IsSuccess;
}
```

The function does not return a `Task`. Its caller does not `await` it. Nothing about its signature says it might take
a while.

### What happens when a function hits an effect?   {#what-happens}
When a function reaches an effect, the engine **suspends** it, performs the effect, and **resumes** it at the very
next line — with every local still in place. That suspension is *durable*: if the process is restarted, redeployed or
killed while the HTTP call is in flight, the function still resumes where it left off. It is not a thread parked in
memory; it is a continuation the platform persisted and will pick up again.

That is a stronger promise than `async` makes. A C# `async` method whose process dies mid-await is simply gone.

### Why is there no `async` colour to propagate?   {#colour}
In C#, `async` is a **colour**. A method that awaits must be `async`, so its callers must `await` it, so *they* must
be `async`, and it spreads outward until it has reached `Main`. You end up marking a hundred functions to describe a
property of one — and then a library forces you into `.Result` and you deadlock a thread pool at 4am.

The colour exists so that a *caller* knows the callee might yield. Here, that is the engine's business rather than the
signature's: any function can suspend, so no function has to advertise it. There is nothing to spread, so there is
nothing to mark.

The practical consequence: **the ability to call out to the world costs you nothing in your function signatures.** You
can add an HTTP call to a function three layers down and not touch a single caller.

### The one place `await` is legal — `Workflow.Run`   {#the-one-await}
`await Workflow.Run("step", …)` is the exception, and it earns it. There, waiting is the thing you are actually saying: *start
this other long-running process, and do not continue until it is finished.* It may be finished in ten seconds or in
three weeks; the `await` is what says you are content to wait either way.

That is a decision about your business process, not a detail of your threading — which is exactly why it is the one
place the word appears.

### If you write `async` or `await` anyway   {#tolerated}
The compiler stops you. `async` is a parse error — *"Osy# has no `async` — effects run in place, so a function or
method is never marked `async`."* `await` is an error everywhere except the one place it means something
(`await Workflow.Run("step", …)`) — *"Osy# has no `await` — effects run in place, so `await` is never needed."* Both messages
point you straight back to this rule. There is no tolerated middle ground: the word suggests a distinction that does
not exist, so the compiler removes the temptation rather than letting it read as meaningful.

## See also       {#see-also}
- [function](https://osysharp.com/reference/function/declaration/) — how a function is written
- [Http.*](https://osysharp.com/reference/http/facade/) — `Http.Get` / `Http.Post`, the effects you call without ceremony
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — long-running processes, where waiting is the point


---

<!-- https://osysharp.com/reference/function/break-continue/ -->

# break / continue

> break leaves the enclosing loop; continue skips the rest of this pass. Inside a switch, break leaves the SWITCH, not the loop around it — the one place this trips people up.

<!-- id: function-break-continue · area: function · stability: stable · html: https://osysharp.com/reference/function/break-continue/ -->

## Summary        {#summary}
`break` leaves the enclosing loop. `continue` abandons this pass and goes to the next one. Both behave exactly as in
C# — including the part that catches people out: **inside a `switch`, `break` leaves the switch, not the loop around
it.**

## Signature      {#signature}
```osy syntax
break;      // stop looping
continue;   // skip the rest of this pass
```

## Description    {#description}

### `continue` — skip this one   {#continue}
```osy title="skipping the rows you do not care about" test app=function-break-continue
entity Order {
  [Required] string Code;
  decimal Total;
  bool Cancelled;
}

decimal LiveTotal() {
  var total = 0m;
  foreach (var o in Order.Where(x => x.Total > 0).ToList()) {
    if (o.Cancelled) { continue; }    // not this one — next
    total += o.Total;
  }
  return total;
}
```

### `break` — stop entirely   {#break}
```osy title="stopping at the first match" test app=function-break-continue
string FirstBig(decimal threshold) {
  var found = "";
  foreach (var o in Order.Where(x => x.Total > 0).ToList()) {
    if (o.Total >= threshold) {
      found = o.Code;
      break;                          // done — no point looking further
    }
  }
  return found;
}
```

### Why didn't `break` leave my loop? — the `switch` trap   {#switch-trap}
This is the one to remember. Inside a `switch` that sits in a loop, `break` ends the **switch**, and execution
continues after it — *inside the same pass of the loop*. It does not leave the loop.

`continue`, by contrast, passes straight through the switch and continues the **loop**:

```osy title="break exits the switch; continue continues the loop" test app=function-break-continue
int SumNonZero(int[] xs) {
  var total = 0;
  for (int i = 0; i < xs.Length; i++) {
    switch (xs[i]) {
      case 0: continue;               // skips to the next i — the loop's next pass
      default: break;                 // leaves the SWITCH; falls through to the += below
    }
    total += xs[i];
  }
  return total;
}
```

If you want to leave the loop from inside a switch, you need a flag, or to restructure the loop — exactly as in C#.
This is not a wart we introduced; it is C#'s rule, and we kept it rather than invent a different one you would have to
learn twice.

## See also       {#see-also}
- [foreach](https://osysharp.com/reference/function/foreach/) · [while](https://osysharp.com/reference/function/while-loop/) · [for](https://osysharp.com/reference/function/for-loop/) — the loops these act on
- [switch](https://osysharp.com/reference/function/switch/) — where `break` means something different


---

<!-- https://osysharp.com/reference/function/const/ -->

# const

> A value fixed at compile time and folded into the places it is used. Declare one at the TOP LEVEL to share it across the whole app, on a component, on a class, or inside one body. Use it to name a magic number so the next reader knows what it means.

<!-- id: function-const · area: function · stability: stable · html: https://osysharp.com/reference/function/const/ -->

## Summary        {#summary}
`const` declares a value fixed at **compile time**. The initializer must be a constant expression — a literal, or
arithmetic over other constants — and the value is folded into every place it is used.

**It goes in any of four places, as in C#:**

```syntax
const int EatSoonDays = 90;                       // top level — every function and page in the app can read it
component Home() { const int Rows = 20; … }       // one component
int F() { const int Limit = 5; … }                // one body
class Rules { public const int Retries = 3; }     // on a class
```

⭐ Because it FOLDS, a `const` goes anywhere a literal goes — **including inside a query predicate**:

```syntax
const int EatSoonDays = 90;
live var due = Item.Where(i => i.Days > EatSoonDays).Count();   // becomes `days > 90`, and lowers to SQL
```

A helper function cannot: `Item.Where(i => i.Days > EatSoonDays())` is refused, because a predicate becomes SQL and
SQL cannot call back into your code. That is the difference between the two, and it is the reason to reach for
`const` when the value never varies.

Its real job is not performance. It is **naming a magic number** so the next person does not have to guess what `0.2`
meant.

## Signature      {#signature}
```osy syntax
const <Type> <NAME> = <compile-time constant>;
```

## Description    {#description}

### How do I give a number a name?   {#naming}
```osy title="a rate with a name" test app=function-const
decimal WithVat(decimal amount) {
  const decimal VatRate = 0.2m;
  return Math.Round(amount * (1 + VatRate), 2);
}
```

`amount * 1.2m` would compute the same total and tell the reader nothing. Six months later, `VatRate` is the
difference between a change that takes a minute and one that takes an afternoon of grepping for `1.2`.

### What may a `const` initializer contain?   {#must-be-constant}
The initializer is evaluated by the compiler, so it cannot read a parameter, a row, or anything decided at run time.
Constants may be built from other constants:

```osy title="constants composed of constants" test app=function-const
decimal Fee(decimal amount) {
  const decimal Base = 2m;
  const decimal Percent = 0.015m;
  const decimal Cap = Base + 50m;          // fine — arithmetic over constants
  var fee = Base + amount * Percent;
  return fee > Cap ? Cap : fee;
}
```

Something that depends on a value only known at run time is not a `const` — it is a [`var`](https://osysharp.com/reference/function/var/).

### `const` vs `var`   {#const-vs-var}
| | `const` | `var` |
|---|---|---|
| Value known at | compile time | run time |
| Can be reassigned | no | yes |
| Good for | a named fixed number, a threshold, a rate | everything else |

## See also       {#see-also}
- [var](https://osysharp.com/reference/function/var/) — a local whose value is computed at run time
- [Typed locals](https://osysharp.com/reference/function/typed-locals/) — an explicitly typed local you can reassign
- [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) — literal suffixes (`m`, `L`) and what they mean


---

<!-- https://osysharp.com/reference/function/execution-side/ -->

# execution side

> Where a function runs. Osy# infers it from the body: a function that reads data runs on the server, a function that touches the router or the theme runs in the browser, and a pure function runs wherever the caller already is.

<!-- id: function-execution-side · area: function · stability: stable · html: https://osysharp.com/reference/function/execution-side/ -->

## Summary        {#summary}

Where a function runs. Osy# infers it from the body: a function that reads data runs on the server, a function that
touches the router or the theme runs in the browser, and a pure function runs wherever the caller already is. You do
not write the side, and you do not wire the round trip — calling a function that runs elsewhere is an ordinary call.

## Signature      {#signature}

```osy syntax
Side = Server | Client | Either   // inferred — never spelled in source
```

## Description    {#description}

Every function, method and constructor has an **execution side**: the place its body runs. There are three.

| Side | Meaning |
|---|---|
| `Server` | The authority. Anything that reads the data store, checks security, or touches a secret. |
| `Client` | The browser. Anything that acts on something only the browser has — the router, the session, the theme. |
| `Either` | Pure work. It runs wherever the caller already is, and never forces a trip across the network. |

**You never declare the side.** The compiler reads the body and works it out, then works out every *caller's* side
from that, and so on up the call graph. The rule is simply: **a function is at most as client-side as the most
server-side thing it can reach.**

```osy title="the side travels up the call graph, not from the top line" syntax
string Greeting(string name) {           // Either — pure. Runs in the browser if that's where you called it.
  return "welcome " + name;
}

User CurrentUser(string email) {         // Server — it reads the data store.
  return Users.Single(u => u.Email == email);
}

string Welcome(string email) {           // Server — because it calls CurrentUser.
  return Greeting(CurrentUser(email).Name);
}
```

`Welcome` is `Server` even though its own body looks pure, because it *reaches* a data read. That is the whole point:
the side is a property of what a function can actually do, not of what its top line looks like.

### Why this matters   {#cross-side-calls}

Calling a function that runs on the other side is still just a call. You write `Login(email, password)` and the
platform does the rest: it evaluates the arguments where you are, suspends, runs the callee on the other side, and
resumes you with the result — including through a `try`/`catch`, which behaves exactly as it would locally.

What the side changes is the **cost**. A call to an `Either` function from a browser action runs *in the browser*, in
process, with no network at all. The same call to a `Server` function is a round trip. Because the side is inferred
rather than assumed, a helper that merely formats a string does not silently cost you a request.

### A body can mix sides   {#mixed-body}

A `Server` function may still do client-located work — show something, ask the user, navigate — and the platform hands
that piece back to the browser and picks up where it left off:

```osy title="one straight-line body that starts server and ends in the browser" syntax
[Page("/orders")]
component OrderPage() {
  action Cancel(Order order) {           // a browser action
    if (Confirm(order)) {                // ↩ runs on the server: it reads and validates…
      Navigation.Go("/orders");          //   …and this line comes back to the browser
    }
  }
}
```

A body with **both** a server anchor and a client anchor is `Server`: it starts on the authority and hands its
client-located parts back. That is not a compromise — it is how a flow can validate on the server and still ask the
user something in the browser, in one straight-line function.

### The standard library is pure, so it runs where you are   {#stdlib}

Calling `Text.Upper`, `Text.Trim`, `Text.Substring` or `string.Join` — and the instance spellings that lower onto
them, like `name.ToUpper()`, `s.Trim()` and `s.Length` — does **not** make a function `Server`. They are pure
operations on a value you already hold, so they run wherever the caller is:

```osy title="stdlib calls are pure, so they cost no round trip" syntax
string Initials(string first, string last) {          // Either — no round trip
  return first.Substring(0, 1) + last.Substring(0, 1);
}
```

The same goes for `name.Contains("x")`, `StartsWith` and `EndsWith`.

Some library calls **are** server-anchored, and for reasons worth stating: `Security.HashPassword` needs the host's
salt generator, `Security.IssueJwt` needs the host's signing key, and `Crypto.Encrypt` needs an encryption key the
browser must never hold. A function that calls one of those is `Server`, as it should be.

Where a library call cannot yet run in the browser, it simply runs on the server — the answer is the same, the call
just costs a round trip. Correctness never depends on which side a pure call lands on: both sides are held to the
same answers, character for character, down to how `Text.Upper` treats the German ß.

### The one thing that is not negotiable   {#queries-are-server}

A query over the **data store** is always `Server` — the data lives on the server, so reading it is a server
operation. An entity query pins its function to the server, always.

A query over a **local list** is not a data read, and does not:

```osy title="a query over a local list is not a data read" syntax
int Cheap(List<Line> lines) {                    // Either — runs in the browser
  return lines.Where(l => l.Price < 10).Count();
}
```

## Examples       {#examples}

```osy title="a pure helper runs in the browser" test app=side-inference
string Initials(string first, string last) {
  return first.Substring(0, 1) + last.Substring(0, 1);
}
```

```osy title="reading data pins a function to the server" test app=side-inference
entity Customer { string Email; string Name; }

string NameFor(string email) {
  return Customer.Single(c => c.Email == email).Name;
}
```

### Pinning it — `[Client]` and `[Server]`   {#pinning}
Side is INFERRED from the body, and that is the normal case. `[Client]` and `[Server]` are an ASSERTION on top of
that inference, and they PIN the answer:

```osy title="an assertion, not a hint — the compiler checks it" syntax
[Client] string Initials(string name) { … }   // must stay client-runnable
[Server] decimal Rate() { … }                 // must stay on the server
```

Two things they buy. **A helper that quietly stops being client-runnable** — someone adds a data read three calls
down — turns into a silent network round trip today, and nothing tells you; pinned, it is a compile error. And they
**resolve ambiguity** for a body whose behaviour depends on which engine runs it: .NET and JS regular expressions
are different dialects, so the same pattern can match differently depending on where the cursor happens to be. There
an `Either` body is the hazard, and a pin in either direction removes it.

⚠ Marking a function both is refused — it runs in one place or the other.

## See also       {#see-also}
- [function](https://osysharp.com/reference/function/declaration/) — declaring a function
- [LINQ over a local list](https://osysharp.com/reference/query/in-memory-linq/) — LINQ over a local list, which stays client-runnable
- [component](https://osysharp.com/reference/ui/component/) — a component, whose actions always begin in the browser


---

<!-- https://osysharp.com/reference/function/for-loop/ -->

# for

> The C-style counting loop. Runs init once, then repeats the body while cond holds, running update after each pass. Any clause may be omitted; for (;;) is infinite. continue runs the update before re-testing.

<!-- id: function-for-loop · area: function · stability: stable · html: https://osysharp.com/reference/function/for-loop/ -->

## Summary        {#summary}
`for (init; cond; update) { … }` is the C-style counting loop. It runs `init` once, then repeats the body
as long as `cond` holds, running `update` after each pass. Any of the three header clauses may be omitted;
`for (;;)` is an infinite loop (exit it with `break`). `continue` runs the `update` before re-testing the
condition — the same as C#.

## Signature      {#signature}
```osy syntax
for (<init>; <cond>; <update>) {
  …
}
// init:   a declaration (loop-scoped) or an expression, or empty
// cond:   a Boolean expression, or empty (= always true)
// update: an expression run after each iteration, or empty
```

## Description    {#description}
- **`init`** runs once before the loop. A declaration there (`int i = 0`) is **scoped to the loop** — it is
  not visible after the `for`.
- **`cond`** is re-tested before each iteration; an empty condition is always true (`for (;;)`).
- **`update`** runs after each body pass **and on `continue`** — so `continue` advances the counter, it does
  not skip it. (This is why `for` is a first-class construct, not a `while` rewrite.)
- **`break`** exits the loop; **`continue`** jumps to the update then the next condition test.
- The `update` is typically `i++` or `i += n` (see [++ / -- (increment / decrement)](https://osysharp.com/reference/function/increment-decrement/) /
  [Compound assignment (+= -= *= /= %= ??=)](https://osysharp.com/reference/function/compound-assignment/)).
- Durable: a suspend inside a `for` body survives serialize/restore — the loop resumes at the right
  iteration and still runs its update.

## Examples       {#examples}
```osy title="for loops" test app=for-loop
int Sum() { int s = 0; for (int i = 0; i < 10; i++) { s = s + i; } return s; }   // 45

// continue still runs the update — sums the even indices (no infinite loop).
int Evens() {
  int s = 0;
  for (int i = 0; i < 10; i++) {
    if (i % 2 == 1) { continue; }
    s = s + i;
  }
  return s;                                   // 0+2+4+6+8 = 20
}

// Nested loops.
int Grid() {
  int s = 0;
  for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) { s = s + i * j; }
  }
  return s;                                   // 9
}

// Infinite for + break (counter advanced in the body).
int Countdown() {
  int i = 0;
  for (;;) { if (i >= 7) { break; } i++; }
  return i;                                   // 7
}
```

## See also       {#see-also}
- [while](https://osysharp.com/reference/function/while-loop/) — the condition-only loop
- [foreach](https://osysharp.com/reference/function/foreach/) — iterate a collection's elements
- [++ / -- (increment / decrement)](https://osysharp.com/reference/function/increment-decrement/) — the usual `update` clause
- [break / continue](https://osysharp.com/reference/function/break-continue/) — loop control


---

<!-- https://osysharp.com/reference/function/foreach/ -->

# foreach

> Walks a collection — a query result, a list, or a parent's children. The normal way to iterate; reach for a for loop only when you need the index.

<!-- id: function-foreach · area: function · stability: stable · html: https://osysharp.com/reference/function/foreach/ -->

## Summary        {#summary}
`foreach` walks a collection, binding each element to a local. It is the normal loop: a materialised query result, a
`List<T>`, or a parent's children all iterate the same way.

## Signature      {#signature}
```osy syntax
foreach (var <item> in <collection>) { … }
```

## Description    {#description}

### Walking a query result   {#query}
Materialise the query with `.ToList()`, then walk it:

```osy title="summing what a query returned" test app=function-foreach
entity Order {
  [Required] string Code;
  decimal Total;
}

decimal TotalOver(decimal threshold) {
  var sum = 0m;
  foreach (var o in Order.Where(x => x.Total > threshold).ToList()) {
    sum += o.Total;
  }
  return sum;
}
```

Note the accumulator is seeded `0m`, not `0` — see [var](https://osysharp.com/reference/function/var/).

### Walking a parent's children   {#children}
A parent's collection member iterates directly. This is the connected path through the object graph, and the reason
you should never fetch children with a separate filtered query ([relations](https://osysharp.com/reference/entity/relations/)):

```osy title="walking the children you already loaded" test app=function-foreach
entity Invoice {
  [Required] string Number;
  [ForeignKey(Invoice)] InvoiceLine[] Lines;
}

entity InvoiceLine {
  [Required] Invoice Invoice;
  decimal Amount;
}

decimal InvoiceTotal(string number) {
  var inv = Invoice.Single(i => i.Number == number);
  var total = 0m;
  foreach (var line in inv.Lines) {
    total += line.Amount;
  }
  return total;
}
```

### Walking a list   {#list}
```osy title="iterating a list" test app=function-foreach
string Join(string csv) {
  var joined = "";
  foreach (var part in Text.Split(csv, ",")) {
    joined += Text.Trim(part);
  }
  return joined;
}
```

### Walking the characters of a string   {#chars}
A string is a sequence of one-character strings, so `foreach` walks it exactly as it walks a list — there is no
separate character type to learn, and `ch` is an ordinary `string` you can compare, append and pass on:

```osy title="a character at a time, and what `ch` is" syntax
int Commas(string line) {
  var n = 0;
  foreach (var ch in line) { if (ch == ",") { n = n + 1; } }
  return n;
}
```

The loop lowers to `Text.Chars(line)`, which you may also call directly when you want the characters as a
`string[]` rather than a loop — `Text.Chars("abc")` is `["a", "b", "c"]`.

### When you need the index   {#index}
`foreach` gives you the element, not its position. When the position is what you are after, use a
[`for`](https://osysharp.com/reference/function/for-loop/) loop.

### How do I stop part-way through?   {#leaving}
`break` and `continue` work as they do in C# — see [break / continue](https://osysharp.com/reference/function/break-continue/).

## See also       {#see-also}
- [for](https://osysharp.com/reference/function/for-loop/) — when you need the index
- [break / continue](https://osysharp.com/reference/function/break-continue/) — leaving a loop, or skipping an element
- [relations](https://osysharp.com/reference/entity/relations/) — why children are walked through the collection
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — producing the result you walk


---

<!-- https://osysharp.com/reference/function/format-specifiers/ -->

# format specifiers

> Formats a number to a string with a .NET format specifier — F2 for two decimal places, N0 for a grouped whole number, C for currency, P for a percentage. Formatting is invariant, so the same value renders identically in the browser and on the server.

<!-- id: function-format-specifiers · area: function · stability: stable · html: https://osysharp.com/reference/function/format-specifiers/ -->

## Summary        {#summary}
A number becomes a string with a **format specifier**: `$"{total:F2}"` renders `1.5` as `"1.50"`. The same specifier
works through `total.ToString("F2")` and `Convert.ToString(total, "F2")` — they are three spellings of one operation.

## Signature      {#signature}
```osy syntax
$"{<value>:<format>}"
<value>.ToString(<format>)
Convert.ToString(<value>, <format>)
```

## Description    {#description}

### The specifiers   {#specifiers}

A specifier is a letter plus an optional precision, e.g. `F2`, `N0`, `P1`. Precision defaults to 2 where it applies.

| Specifier | `1234.5678` renders as | What it is |
|---|---|---|
| `F2` | `1234.57` | **fixed-point** — the everyday one, and what money wants |
| `N2` | `1,234.57` | **number** — same as `F`, plus group separators |
| `C` | `¤1,234.57` | **currency** |
| `P2` | `123,456.78 %` | **percent** — multiplies by 100 |
| `E` | `1.234568E+003` | **scientific** |
| `G` | `1234.5678` | **general** — the plain digits |
| `D5` | *(integers only)* `00042` | **decimal digits**, zero-padded |
| `X` | *(integers only)* `2A` | **hexadecimal** |

Three of these behave in ways worth knowing before you rely on them:

- **Currency uses `¤`, the generic currency sign — not `$`.** Formatting is invariant (see below), and the invariant
  culture has no country, so it has no currency symbol either. If you want `$` or `€`, write it: `$"${total:N2}"`.
- **A negative currency is wrapped in parentheses:** `-1.5` renders as `(¤1.50)`, not `-¤1.50`.
- **`D` and `X` are integer-only.** Applied to a decimal they raise an error, even for a whole number like `0`.

### Rounding   {#rounding}

Formatting rounds **away from zero**: `1.005` to two places is `1.01`, and `-1.005` is `-1.01`. Because a `decimal`
holds exact base-10 digits, that is the digit you actually wrote — not the nearest binary approximation of it. A
value that rounds to zero prints as `0`, never `-0`.

### It renders the same everywhere   {#invariant}

Formatting is **invariant**: it does not consult the machine's locale. A price is the same string in the browser, on
the server, in a log line, and in a test — and it does not change when a user in another country opens the page.

That is also why formatting runs **in the browser**, with no round trip: rendering a grid of prices is local work.

### Custom patterns   {#custom-patterns}

A custom pattern like `"#,##0.00"` or `"0.00"` also works, and does exactly what it does in C#. Note that a custom
pattern is formatted on the server, so a UI action using one costs a network round trip where a standard specifier
would not — prefer `N2` over `#,##0.00` when they give the same answer.

## Examples       {#examples}
```osy title="a price in a grid" test app=text-search
string PriceLabel(decimal amount) {
  return $"{amount:N2}";
}
// PriceLabel(1234.5m)  ->  "1,234.50"
```

```osy title="a percentage and a padded reference" test app=text-search
string Summary(decimal rate, int reference) {
  return $"{rate:P1} · ref {reference:D6}";
}
// Summary(0.0825m, 42)  ->  "8.3 % · ref 000042"
```

## See also       {#see-also}
- [String interpolation & format specifiers](https://osysharp.com/reference/function/string-interpolation/) — the `$"…"` string itself
- [decimal](https://osysharp.com/reference/types/decimal/) — why exact base-10 digits are what makes `F2` trustworthy
- [execution side](https://osysharp.com/reference/function/execution-side/) — why this runs in the browser


---

<!-- https://osysharp.com/reference/function/declaration/ -->

# function

> A function is a top-level unit of work, written like a C# method — a return type, a name, typed parameters, a body. It runs transactionally: the rows it writes are committed together when it finishes.

<!-- id: function-declaration · area: function · stability: stable · html: https://osysharp.com/reference/function/declaration/ -->

## Summary        {#summary}
A function is the unit of work: a return type, a name, typed parameters and a body, written exactly like a C# method
but declared at the top level of a file. It is where you create rows, query them and decide things.

It runs **transactionally**. The rows a function writes are committed together when it returns, so a function that
fails half-way leaves nothing behind. There is no `Save()` and no `Commit()` to remember.

⚠ **That is true of a FUNCTION and not of a component `action`.** An action runs in the page's optimistic overlay,
where a write renders immediately and is not yet persisted — so an action ends with `UnitOfWork.Commit()`, and the
examples on this page do not because they are functions. If you are looking at a body inside a `component`, see
[creating & saving data](https://osysharp.com/reference/ui/data-mutation/); the rule there is the opposite of the one here, and knowing which body you are in is the whole
of it.

## Signature      {#signature}
```osy syntax
<ReturnType> <Name>(<Type> <param>, …) {
  <statements>
}

```

## Description    {#description}

### How do I declare a function?   {#declaring}
`void` when it returns nothing, a type when it returns something. Parameters are typed and camelCase; the function
name is PascalCase:

```osy title="a function that writes, and one that reads" test app=function-declaration
entity Order {
  [Required] string Code;
  decimal Total;
}

void PlaceOrder(string code, decimal total) {
  var o = new Order { Code = code, Total = total };
}

decimal OrderTotal(string code) {
  var o = Order.Single(x => x.Code == code);
  return o.Total;
}
```

### It commits as one unit   {#transactional}
Everything a function writes lands together, or not at all. That is what lets you write the obvious thing:

```osy title="two rows, one outcome" test app=function-declaration
entity AuditLine {
  [Required] string Message;
}

void PlaceAndLog(string code, decimal total) {
  var o = new Order { Code = code, Total = total };
  var a = new AuditLine { Message = "placed " + code };
  // both rows commit together — there is no state where the order exists and the log line does not
}
```

If the function faults — a constraint violation, an invariant, a division by zero — **neither row is written.** You
do not have to unwind anything by hand.

### There is no `async`   {#no-async}
A function that reaches outside the database — an HTTP call, an LLM completion — is written **exactly like any other
function**. There is no `async`, no `Task<T>`, and no colour to keep track of:

```osy title="a function that calls out — no async anywhere" test app=function-declaration-async
app Shop {
  model "model/**/*.osy";
  use Osysharp.Http;
}

string Fetch(string url) {
  var r = Http.Get(url);           // just a call — the engine suspends and resumes around it
  return r.IsSuccess ? r.Body : "";
}
```

Those outward calls are **effects**, and the platform handles them: when a function hits one, the engine suspends it,
performs the effect, and resumes the function where it left off — even if that means surviving a process restart in
between. You do not have to mark the function, and neither does its caller.

This is why C#'s `async` is absent rather than merely optional. `async` exists to colour a function so its *callers*
know to await it, and that colour spreads until it has infected everything it touches. Here the durability is the
engine's job, not the signature's, so there is nothing to spread.

`await` appears in exactly one place in the language — `Workflow.Run()` — where you are genuinely waiting for another
long-running thing to finish, and want to say so. If you are coming from C#, [async / await — why Osy# has neither](https://osysharp.com/reference/function/async-await/) is the page to
read: it is the first habit to unlearn.

### Where may a function be declared?   {#where}
**At the top level of a file — that is what a `function` is.** It is not nested in anything, and there is no
namespace, module or class you have to put it inside first. A file may hold as many as you like, beside its entities
and components. Not inside an entity, though: an entity body holds data, and behaviour sits beside it.

Two things that LOOK like the same question are not, and the difference is what each one commits:

| you write it… | what it is | where it runs | what saves it |
|---|---|---|---|
| at the top level of a file | a **function** | inferred from its body — see [execution side](https://osysharp.com/reference/function/execution-side/) | itself, on return |
| inside a `component` | a **method** of that component | with the component | the page's `UnitOfWork.Commit()` |
| inside a `class` | a **method** of that class | wherever it is called from | its caller |

All three are written identically — a return type, a name, a typed parameter list, a body — so the enclosing
declaration is the only thing that decides which you have. There is no `function` keyword to write and no `method`
keyword either; see [Writing a component — what differs from C#](https://osysharp.com/reference/ui/csharp-differences/) for why.

```osy title="all three, in one file" test app=function-declaration-where
int Doubled(int n) { return n * 2; }                            // a function

class Rates { public decimal WithVat(decimal net) { return net * 1.25m; } }   // a class method

[Page("/counter")] [AllowAnonymous] [Render(CSR)]
component Counter() {
  int n = 1;
  int Quadrupled(int x) { return x * 4; }                       // a component method
  action Bump() { n = Quadrupled(n); }
  render { Text("n=" + n); }
}
```

And the top-level form beside the data it works on:

```osy title="behaviour lives beside the data, not inside it" test app=function-declaration
entity Product {
  [Required] string Name;
  decimal Price;
}

decimal PriceWithVat(Product p, decimal rate) {
  return Math.Round(p.Price * (1 + rate), 2);
}
```

If you want behaviour *attached* to a type — a method with a receiver — that is a [class method](https://osysharp.com/reference/class/methods/).

## See also       {#see-also}
- [var](https://osysharp.com/reference/function/var/) — locals inside the body
- [if / else](https://osysharp.com/reference/function/if/) · [foreach](https://osysharp.com/reference/function/foreach/) · [while](https://osysharp.com/reference/function/while-loop/) — control flow
- [class methods](https://osysharp.com/reference/class/methods/) — behaviour attached to a type
- [async / await — why Osy# has neither](https://osysharp.com/reference/function/async-await/) — why there is no `async`, and where the one `await` lives
- [Running tests](https://osysharp.com/reference/testing/running-tests/) — how you run one


---

<!-- https://osysharp.com/reference/function/if/ -->

# if / else

> Conditional branching, exactly as in C#. The condition must be a bool — there is no truthiness, so a null or a number is not a condition.

<!-- id: function-if · area: function · stability: stable · html: https://osysharp.com/reference/function/if/ -->

## Summary        {#summary}
`if` / `else if` / `else`, exactly as in C#. The condition must be a **`bool`** — there is no truthiness. A string, a
number or a null is not a condition, and writing one is a compile error rather than a subtle bug.

## Signature      {#signature}
```osy syntax
if (<bool>) { … }
else if (<bool>) { … }
else { … }
```

## Description    {#description}

### How do I write an `if` / `else` chain?   {#branching}
```osy title="grading a total" test app=function-if
string Band(decimal total) {
  if (total >= 1000m) {
    return "large";
  } else if (total >= 100m) {
    return "medium";
  } else {
    return "small";
  }
}
```

### The condition is a bool, always   {#no-truthiness}
There is no "non-empty string is true" and no "non-zero is true". Say what you mean:

```osy title="testing for a value" test app=function-if
entity Contact {
  [Required] string Name;
  string Phone;
}

string Reach(Contact c) {
  if (c.Phone != null) { return c.Phone; }      // not `if (c.Phone)`
  return "no phone";
}

bool IsBig(int count) {
  if (count > 0) { return true; }               // not `if (count)`
  return false;
}
```

This is stricter than a dynamic language, and it is the strictness that pays: `if (count)` and `if (count > 0)` mean
the same thing right up until `count` is `-1`.

### Choosing a value rather than a branch — `?:`   {#ternary}
For a value rather than a branch, `?:` reads better than four lines of `if`:

```osy title="choosing a value" test app=function-if
string Label(bool paid) {
  return paid ? "paid" : "outstanding";
}
```

### Too many `else if`s? — reach for `switch`   {#many-cases}
A chain of `else if` over the same value is usually a [`switch`](https://osysharp.com/reference/function/switch/) — especially over an
[enum](https://osysharp.com/reference/enum/declaration/), where the compiler can then tell you when you have missed a case.

## See also       {#see-also}
- [switch](https://osysharp.com/reference/function/switch/) — branching over many values of one expression
- [enum](https://osysharp.com/reference/enum/declaration/) — the closed sets a switch is exhaustive over
- [while](https://osysharp.com/reference/function/while-loop/) — repeating while a condition holds


---

<!-- https://osysharp.com/reference/function/nameof/ -->

# nameof

> The C# compile-time name fold: validates the symbol and folds to its simple-name string literal — the last identifier of the chain. Invalid symbols are compile errors. No runtime or stored surface.

<!-- id: function-nameof · area: function · stability: stable · html: https://osysharp.com/reference/function/nameof/ -->

## Summary        {#summary}
`nameof(symbol)` validates the symbol and folds — at compile time — to its simple-name **string
literal**: the LAST identifier of the chain, exactly C#. There is no runtime or stored surface: the
persisted node is an ordinary String literal (like const-inlining, the decompiled form shows the folded
string).

## Signature      {#signature}
```osy syntax
nameof(<name>)            // a local, param, entity set, or module
nameof(<Type>.<Member>)   // the type-member form → "Member"
nameof(<value>.<Member>)  // the value-member form → "Member"
```

## Description    {#description}
Valid symbols, each folding to the last identifier:

- a **parameter** or **local** — `nameof(count)` → `"count"`;
- an **entity / type name** — `nameof(Order)` → `"Order"`; a namespaced set folds to the simple name
  (`nameof(osy.User)` → `"User"`);
- the **type-member form** — `nameof(Order.Total)` → `"Total"`;
- the **value-member form** — `nameof(o.Total)` → `"Total"` (the chain must resolve);
- **stdlib modules** — `nameof(Math)` → `"Math"`.

**Invalid symbols are compile errors**, as in C#: `nameof(missing)` →
`nameof: unknown symbol 'missing'`; `nameof(o.Nope)` → `nameof: cannot resolve 'Nope' …`;
`nameof(1 + 2)` → `nameof requires a simple name or member access`.

A local variable actually **named** `nameof` shadows the operator (C# contextual-keyword behavior).

Typical use: validation and error messages that survive renames.

## Examples       {#examples}
```osy title="folds" test app=nameof-examples
entity Order { decimal Total; }

string Examples(Order o, int count) {
  var total = 5;
  var a = nameof(count);        // "count"   — a parameter
  var b = nameof(total);        // "total"   — a local
  var c = nameof(Order);        // "Order"   — an entity name
  var d = nameof(Order.Total);  // "Total"   — the type-member form
  var e = nameof(o.Total);      // "Total"   — the value-member form
  return a + b + c + d + e;
}
```

## See also       {#see-also}
- [const](https://osysharp.com/reference/function/const/) — the other compile-time fold (nameof stores exactly like an inlined const)


---

<!-- https://osysharp.com/reference/function/secret-read/ -->

# reading a secret's value (Secret.Name)

> `Secret.Name` in a function body evaluates to the declared secret's value — the key itself, as a string, read at the moment it is used. It is how a webhook signature gets its shared key or an outbound call gets its token. The name must be one your app declares in `app.Secrets`; an undeclared name is a compile error, not a runtime null. The value never enters your source, and the compiler refuses the four ways it could escape the server — returning it, storing it, reading it in anything the browser runs, or handing it to a sink such as `Log`.

<!-- id: function-secret-read · area: function · stability: stable · html: https://osysharp.com/reference/function/secret-read/ -->

## Summary        {#summary}
`Secret.Name` reads a **declared secret's value** inside a function body. It evaluates to the stored string — an API
key, a shared signing key, a token — at the moment the line runs.

The same `Secret.Name` handle also appears in config slots such as `app.DefaultModel`'s `ApiKey`
([declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/)). There it *points at* a secret for the platform to resolve; in a function body it *is* the value,
because that is what code needs in order to sign, compare, or send it.

```osy syntax
var expected = Crypto.HmacSha256Hex(Secret.CarrierWebhook, payload);
```

## Signature      {#signature}
```osy syntax
Secret.<Name> -> string
```

`<Name>` is written as an identifier, exactly as declared — `Secret.CarrierWebhook` for
`new Secret("CarrierWebhook")`. It is not a string, not a lookup, and takes no arguments.

## Description    {#description}
A secret has to be declared before it can be read. `app.Secrets = [ new Secret("CarrierWebhook") ];` declares it;
`osy secret set CarrierWebhook` gives it a value on your machine ([Giving a secret its value](https://osysharp.com/reference/local/giving-a-secret-its-value/)); a deployed
app's values are supplied by whoever operates its platform.

**An undeclared name is a compile error.** That is deliberate and it is worth knowing why: a mistyped credential that
resolved to nothing would make every signature check compare against a key of empty string, which fails exactly like
a wrong signature from the sender. You would go looking at the sender, and you would keep looking. The compiler
refuses the typo instead.

**A declared secret with no value throws when it is read**, and says which secret and how to set it. This is also not
an empty string, for the same reason — an unset key is the ordinary state of a fresh checkout, and it must not
silently degrade into a check that always fails.

### It is server-side, always      {#server-side}
Reading a secret is a **server** operation. A credential has no client-side producer, so an expression containing
`Secret.Name` pins its containing code to the server ([execution side](https://osysharp.com/reference/function/execution-side/)) — it can never be evaluated in a
browser, and no component that runs there can reach one.

### What you may not do with the value      {#confinement}
The read gives you the plaintext, so the compiler governs where that plaintext may go. **Four things are compile
errors**, not warnings — a warning can be ignored and a leaked credential cannot:

| refused | why |
|---|---|
| **returning** it, from any function, method or constructor | a return value goes to the caller, and a caller can be a page action — the key would land in component state and on the screen |
| **storing** it in an entity field | a stored credential is readable by everything that can read that row, rides into backups and exports, and can no longer be rotated by `osy secret set` |
| **reading** it in anything sent to the browser — an `action`, a component `method`, a render slot | evaluating it there means the plaintext was shipped |
| **handing** it to an effect — `Log.*`, a file write, the clipboard, a dialog, a topic publish, a workflow event payload | each of those puts what it is given somewhere durable or observable. `Log` is the one that surprises people: it is **dual-sided**, so a client log line is in the visitor's own browser console as well as in `osy logs` |

```osy syntax
string SigningKey() { return Secret.CarrierWebhook; }   // ✗ returned
new AuditRow { Token = Secret.CarrierWebhook };         // ✗ stored
Log.Information(Secret.CarrierWebhook);                 // ✗ logged — and shown, on the client
```

**What stays legal is the whole point of the feature**: read the secret and *use* it, then let the RESULT travel.
Signing with it, comparing with it, and presenting it to the service it authenticates to are all ordinary code — both
examples below do exactly that. A signature or a token you were issued is not a credential of yours, so it may be
returned, stored and logged like any other string.

The rule follows the value through locals, concatenation, interpolation, ternaries and string helpers, so renaming it
on the way out does not evade it. It stops at a call into **your own** functions: `Sign(Secret.K, msg)` hands the key
to code the compiler cannot see, and what that code does with it is yours to get right. So this confines an
*accidental* leak, not a determined one — and it could not be otherwise, since giving the key to an outbound call is
the sanctioned use.

What is persisted is the ciphertext on the secret's own row, and nothing else — a function's expression tree carries
only the NAME.

### Do secrets work under `osy test`?      {#in-a-test}
`osy test` runs against a throwaway branch of your app built from source, and your project's `.secrets` values travel
with the run — so a `[Test]` that exercises a signature check reads the same key the app does, and the credential
path is testable rather than the one part you have to take on faith.

## Examples       {#examples}
Verifying a signed carrier webhook — the key comes from the secret store, and the tag is compared in constant time:

```osy title="verify an inbound signature" test app=function-secret-read
app.Secrets = [ new Secret("CarrierWebhook") ];

bool IsAuthenticScan(string trackingNumber, string location, string signature) {
  var expected = Crypto.HmacSha256Hex(Secret.CarrierWebhook, trackingNumber + "|" + location);
  return Crypto.FixedTimeEquals(expected, signature);
}
```

Sending one outbound — the same read, used as a bearer token rather than a signing key:

```osy title="authenticate an outbound call" test app=function-secret-read-outbound
// `use` is a MANIFEST declaration — it belongs in your app.osy, not in a model file.
app Dispatch {
  model "model/**/*.osy";
  use Osysharp.Http;
}

app.Secrets = [ new Secret("DispatchApi") ];

string FetchManifest(string depot) {
  var headers = new Dictionary<string, string>();
  headers.Add("Authorization", "Bearer " + Secret.DispatchApi);
  var r = Http.Get("https://api.example.com/manifests/" + depot, headers);
  return r.IsSuccess ? r.Body : "";
}
```

## See also       {#see-also}
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — `app.Secrets`, where a secret is declared and named
- [Giving a secret its value](https://osysharp.com/reference/local/giving-a-secret-its-value/) — `osy secret set`, and which secrets are still empty
- [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/) — verifying a signature with the key you just read, in constant time
- [execution side](https://osysharp.com/reference/function/execution-side/) — why an expression that reads a secret pins its code to the server


---

<!-- https://osysharp.com/reference/function/switch/ -->

# switch

> Branch on a value against constant case labels. Only the matched section runs (no fall-through); a default section handles the rest. break exits the switch; continue continues the enclosing loop.

<!-- id: function-switch · area: function · stability: stable · html: https://osysharp.com/reference/function/switch/ -->

## Summary        {#summary}
`switch (scrutinee) { … }` branches on a value against constant `case` labels, running the first matching
section (or `default` if none match). Only the matched section runs — there is **no implicit
fall-through** — so a trailing `break` is idiomatic but optional. `break` exits the switch; `continue`
inside a switch continues the **enclosing loop**.

## Signature      {#signature}
```osy syntax
switch (<scrutinee>) {
  case <const>:
  case <const>:      // stacked labels share a body
    <statements>
    break;           // optional — sections never fall through
  default:
    <statements>
}
```

## Description    {#description}
- **Case labels are compile-time constants** — literals, enum members, or `const`s — comparable to the
  scrutinee's type (`int`, `string`, `enum`, `bool`). A non-constant or type-incompatible label is a
  compile error.
- **Stacked labels** (`case 1: case 2:`) share one body.
- **`default`** runs when no case matches; at most one is allowed, and it may appear anywhere.
- **No fall-through**: only the matched section runs, then the switch exits. A trailing `break` is
  accepted (and idiomatic) but not required — unlike C#, Osy# sections never fall through, so a missing
  break is not an error.
- **`break`** exits the switch (it is a break boundary, not a loop). **`continue`** inside a switch that
  sits in a loop continues that **loop** (it passes through the switch) — exactly C#.
- A function whose value is produced entirely by a switch needs an exhaustive switch (a `default` where
  every section returns) to satisfy definite-return.
- Durable: a suspend inside a case body survives serialize/restore.

## Examples       {#examples}
```osy title="switch" test app=switch-examples
string Grade(int score) {
  switch (score) {
    case 5: case 4: return "pass";     // stacked labels
    case 3: return "marginal";
    default: return "fail";
  }
}

int Code(string s) {
  switch (s) {                          // switch on a string
    case "red": return 1;
    case "green": return 2;
    default: return 0;
  }
}

// break exits the SWITCH, not the loop; continue continues the LOOP.
int Scan(int[] xs) {
  int total = 0;
  for (int i = 0; i < xs.Length; i++) {
    switch (xs[i]) {
      case 0: continue;                 // skip zeros — continues the for loop
      default: break;                   // exits the switch, falls to the += below
    }
    total = total + xs[i];
  }
  return total;
}
```

## See also       {#see-also}
- [if / else](https://osysharp.com/reference/function/if/) — the two-way / chained conditional
- [break / continue](https://osysharp.com/reference/function/break-continue/) — how break and continue behave in loops vs switch
- [for](https://osysharp.com/reference/function/for-loop/) — the loop `continue` targets when inside a switch


---

<!-- https://osysharp.com/reference/function/switch-expression/ -->

# switch expression

> Choose a VALUE by matching a subject against patterns. Arms are tried in order and the first match wins. Patterns are a constant, a relational comparison, a type test, or the `_` discard. Over a closed vocabulary — an enum or a bool — a missing arm is a compile error rather than a runtime throw.

<!-- id: function-switch-expression · area: function · stability: stable · html: https://osysharp.com/reference/function/switch-expression/ -->

## Summary        {#summary}
`subject switch { … }` is an EXPRESSION: it produces a value. Arms are tried top to bottom and the first
one that matches wins. It is the form to reach for wherever a value is wanted rather than a statement —
notably inside a render block, which takes no statements.

Over a **closed vocabulary** (an `enum`, or a `bool`) a switch expression with no `_` arm must cover every
member, and a gap is a **compile error**.

## Signature      {#signature}
```osy syntax
subject switch {
  Member        => value,     // a CONSTANT — an enum member or a literal
  >= 1000.0     => value,     // a RELATIONAL comparison: < <= > >=
  Circle c      => value,     // a TYPE pattern, with a required binding
  _             => value,     // the DISCARD — matches anything
}
```

## Description    {#description}
**Arms are tried in order, and order is the whole design of a relational ladder.** `1500000` satisfies both
`>= 1000000` and `>= 1000`; writing the larger bound first is what makes it answer `"M"`.

**A relational arm compares against a constant** and may use any of `<`, `<=`, `>`, `>=`. There is no `==`
form because a bare constant arm already means equals.

**Exhaustiveness is checked only where a vocabulary is closed.** An `enum` and a `bool` have a knowable set
of values, so every one must have an arm or the compile fails. A number does not, so a switch over one
always needs `_` — including a relational ladder, because proving that `< 0` and `>= 0` between them cover
every number is arithmetic the compiler does not attempt. It asks for a `_` you may not strictly need
rather than claiming a gap is covered when it is not.

> **A missing enum arm is an ERROR, not a warning.** This is a deliberate divergence from C#, which warns at
> compile time and throws at run time. A throw inside a render expression is a blank page instead of a
> message, so the check that prevents it has to be the one that cannot be ignored.

**The subject is read once per arm test**, so it must be re-readable at no cost — a name, a member access,
an index. Anything that could do work (a call, an `await`) is refused, and the refusal names the one-line
fix: assign it to a local first.

A `when` guard is not part of this form. Use an `if` for that.

## Examples       {#examples}
```osy title="a relational ladder, an enum switch, and a type pattern" test app=switch-expression-examples
public enum MarkKind { Line, Area, Column }

// RELATIONAL — the compact-number ladder. The larger bound comes first because the first match wins.
string Compact(int n) {
  return n switch { >= 1000000 => "M", >= 1000 => "K", _ => "" };
}

// All four ordering operators.
string Band(int n) {
  return n switch { < 0 => "neg", <= 10 => "small", > 100 => "big", _ => "mid" };
}

// A CONSTANT arm over an enum. No `_`: every member has an arm, which is what makes adding a member to
// the vocabulary a build failure at each site that must decide about it.
string Family(MarkKind k) {
  return k switch { Line => "stroke", Area => "stroke", Column => "fill" };
}

// A bool is a closed vocabulary too, so both cases are exhaustive with no `_`.
string YesNo(int n) { return (n > 0) switch { true => "yes", false => "no" }; }
```

```osy title="a TYPE pattern binds the narrowed value" test app=switch-expression-types
class Shape { public string Name; }
class Circle : Shape { public double Radius; }

// `Circle c` binds `c` at the narrower type, so `c.Radius` reads inside that arm.
string Describe(Shape s) {
  return s switch { Circle c => $"circle r={c.Radius}", _ => s.Name };
}
```

## See also       {#see-also}
- [switch](https://osysharp.com/reference/function/switch/) — the switch STATEMENT, which branches control rather than producing a value
- [if / else](https://osysharp.com/reference/function/if/) — the two-way conditional, and where a `when`-guard-shaped test belongs
- [Testing which class a value is](https://osysharp.com/reference/class/type-tests/) — `is` / `as` / a cast / `OfType<T>()`, the other places a type is asked about
- [Enums](https://osysharp.com/reference/enum/index/) — why a closed vocabulary is what makes exhaustiveness checkable


---

<!-- https://osysharp.com/reference/function/throw/ -->

# throw

> Raises a fault. It ends the function immediately, and the rows the function wrote are discarded rather than half-written. The exception types are a closed set you do not add to — usable as ordinary types too, so a variable, a field or a parameter can hold one. CATCHING one is different: the refused row stays staged, and the catch has to say what happens to it.

<!-- id: function-throw · area: function · stability: stable · html: https://osysharp.com/reference/function/throw/ -->

## Summary        {#summary}
`throw` raises a fault. The function stops where it stands, and **the rows it wrote are discarded** — a function that
throws leaves nothing half-written behind it.

You throw one of a **closed set of types**. There is no `class MyException` to declare:

```osy title="refuse, and say why" test app=function-throw
entity Order {
  [Required, Unique, MaxLength(20)] string Code;
  decimal Total;

  // Every entity states who may touch it — with no `security { }` block it is denied to everyone.
  // A real app scopes these grants to a user; an example still has to declare them, because an example
  // that could not actually run is not an example. See the security guide.
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

void Refund(string code, decimal amount) {
  var order = Order.SingleOrDefault(o => o.Code == code);
  if (order == null) { throw new NotFoundException($"no order '{code}'"); }
  if (amount > order.Total) { throw new ValidationException("a refund cannot exceed the order total"); }

  order.Total = order.Total - amount;
}
```

## Signature      {#signature}
```osy syntax
throw new Exception("<message>");                  // the base type — the catch-all
throw new NotFoundException("<message>");          // asked for a thing that is not there
throw new ValidationException("<message>");        // the input or the resulting row is not acceptable
throw new ConflictException("<message>");          // it clashes with the current state of the data
throw new OAuthConnectionFailedException("<msg>"); // an external authorization handshake failed
```

## Description    {#description}

### Which types are there?   {#the-types}
The vocabulary is closed. Naming an unknown type is a compile error that lists every one you may use **and what
raises it**, so you cannot misspell your way into a silent catch-all:

| Type | What it means | Also raised for you by |
|---|---|---|
| `Exception` | the base. Every other type is one, so `catch (Exception e)` catches everything | — |
| `NotFoundException` | you asked for something that does not exist | — |
| `ValidationException` | the input, or the row you are about to write, is not acceptable | a broken [[entity-constraints\|constraint]] or [[entity-invariants\|invariant]] — including a duplicate under `[Unique]` |
| `ConflictException` | it cannot be done given the current state of the data | two writers colliding — see [ConcurrencyCheck](https://osysharp.com/reference/entity/concurrency-check/) |
| `OAuthConnectionFailedException` | an external authorization handshake failed | the OAuth surface |
| `NotAuthorized` | the caller may not do this — and the one to throw when your own function refuses on authorization grounds | a workflow event's `[Authorize]`, or a slot's candidate gate |
| `RequirementsNotMet` | the WORK is not finished, which is not the same as not being allowed | a slot deposit whose `Requires` criteria are unmet |
| `DeviceUnavailableException` | the browser would not hand over a camera or microphone. `ex.Message` says which of the four reasons | `Camera.Start()` / `Mic.Start()` |
| `WorkflowError` | an awaited child workflow reached a `terminal error`. **Catch-only** — throwing it is refused | the engine |
| `WorkflowCancelled` | an awaited child workflow reached a `terminal cancel`. **Catch-only** — throwing it is refused | the engine |

The point of the closed set is that a `catch` has a **finite, knowable** set of things to match on, and every type
the platform raises on your behalf is in it. That is what lets you catch a constraint violation as an ordinary
`ValidationException` ([try / catch / finally](https://osysharp.com/reference/function/try-catch/)) rather than by inspecting a database error.

⚠ **`ValidationException` is the duplicate one, however much English disagrees.** A row refused by `[Unique]` is the
entity's own declared rule saying no, so it is a `ValidationException` — not a `ConflictException`, which is about
what the DATA currently says. The two catch differently and the English pulls the wrong way, which is why the
compile error that lists these names lists what raises each beside it.

### Holding one in a variable   {#as-a-type}
A fault type is an **ordinary type**: it may stand wherever a type name may, so a local, a field, a parameter, a
return type or a generic argument can hold one. Assignment follows the same rule a typed `catch` does — every
subtype widens to `Exception`, and nothing narrows back without you saying which you meant.

```osy title="hold the fault and act on it after the try" test app=function-throw-astype
string Latest(string code) {
  Exception? caught = null;
  try { throw new ConflictException($"'{code}' moved under you"); }
  catch (Exception e) { caught = e; }
  return caught == null ? "fine" : $"{caught.Type}: {caught.Message}";
}
```

### Catching a constraint violation is not the end of it   {#the-refused-row}
⚠ **The row the database refused is still there when your `catch` runs, and doing nothing about it is the one shape
that does not work.** This is the trap, because the code that falls into it is the code everybody writes first:

```osy title="the catch runs — and the 409 never reaches the caller" syntax
int Book(string code) {
  try {
    var b = new Order { Code = code, Total = 10m };
    return 201;
  } catch (ValidationException e) {
    return 409;              // ⛔ the catch DOES run. The refused Order is still staged, so the function
  }                          //    then faults at its own boundary with the same violation, after this
}                            //    `return` has already happened. The caller gets the platform's message.
```

Nothing is dropped for you, and that is deliberate: **it is your data.** A form that is still open on screen must be
able to take the 409, let the user change the value, and save again — so the platform holds the row and leaves the
decision to the app, because only the app knows whether its form closed as part of saving.

There are **three** things you can do about it, and which one is right is a question about your UI:

| Your form… | Do this | Why |
|---|---|---|
| **stays open** after a failed save | **amend the row** in the catch | the end settlement persists the repair — one write, the value the user fixed |
| **stays open**, and the app should say so in its own words | **`throw new ConflictException("…")`** | a throw discards ([[#rollback]]); your sentence reaches the caller and nothing is written |
| **closes** as part of save — there is nothing to get back to | **`UnitOfWork.Discard()`** first, then return | the row is gone, and the function returns your status code normally |

```osy title="all three, and each is a complete answer" test app=function-throw
// The form stays open: fix the value the user gave and let the settlement write it.
string AmendAndKeep(string code) {
  var o = new Order { Code = "provisional", Total = 10m };
  try {
    o.Code = code;
    return "written";
  } catch (ValidationException e) {
    o.Code = "provisional-2";
    return "amended";
  }
}

// Say it in the app's own words. Nothing is written.
string RefuseInMyOwnWords(string code) {
  try {
    var o = new Order { Code = code, Total = 10m };
    return "created";
  } catch (ValidationException e) {
    throw new ConflictException($"the code '{code}' is already spoken for");
  }
}

// The form closed on save. Decline the row explicitly, then answer for yourself.
int DeclineTheRow(string code) {
  try {
    var o = new Order { Code = code, Total = 10m };
    return 201;
  } catch (ValidationException e) {
    UnitOfWork.Discard();
    return 409;
  }
}
```

Proved, not asserted — the discard shape returns its own status code, and writes nothing:

```osy title="a declined row is gone, and the function returns normally" run app=function-throw
[Test]
void Discarding_in_the_catch_lets_the_function_answer_for_itself() {
  var tooLong = "0123456789012345678901234567890123456789";   // > MaxLength(20)

  Assert.Equal(409, DeclineTheRow(tooLong));   // the function's OWN status code, not the platform's fault
  Assert.Empty(Order.ToList());                // and nothing was written
}
```

⭐ **`osy lint` finds the fourth shape for you** — the catch that repairs nothing, rethrows nothing and discards
nothing — as `data-caught-write-fault-left-staged`, and its remedy names all three of the above.

**Why you cannot declare your own:** a fault's *type* is what a `catch` matches on, and its *message* is what a human
reads. A bespoke type would carry no more information than the message already does, and it would not survive leaving
the app — see below.

### A throw discards what the function wrote   {#rollback}
This is the part worth internalising. A function is transactional ([function](https://osysharp.com/reference/function/declaration/)), and a throw is the abort:

```osy title="the first row is not left behind" test app=function-throw
void PlaceTwo(string first, string second) {
  var a = new Order { Code = first, Total = 10m };

  if (second == "") { throw new ValidationException("the second code is required"); }

  var b = new Order { Code = second, Total = 20m };
}
```

If `second` is empty, **neither order exists.** The first `new Order` was already written in the ordinary sense — it is
simply never committed. You do not unwind it, and there is no state in which the caller can observe it.

Proved, not asserted:

```osy title="a fault leaves nothing behind" run app=function-throw
[Test]
void A_throw_discards_the_rows_the_function_had_written() {
  Assert.Throws<ValidationException>(() => PlaceTwo("A1", ""));

  Assert.Empty(Order.ToList());   // NOT one row — the first `new Order` went with it
}

[Test]
void The_type_is_what_a_caller_matches_on() {
  Assert.Throws<NotFoundException>(() => Refund("nope", 1m));
}
```

### `throw` ends a path   {#definite-return}
A `throw` satisfies the compiler's "all paths return a value" rule, exactly as it does in C#. A guard clause that
throws needs no `else`:

```osy title="a guard clause, with no else" test app=function-throw
decimal TotalOf(string code) {
  var order = Order.SingleOrDefault(o => o.Code == code);
  if (order == null) { throw new NotFoundException($"no order '{code}'"); }

  return order.Total;   // reachable only when the order exists — no `else`, no null check
}
```

### What a caller outside the app sees   {#the-boundary}
Inside the app, a throw is caught by type ([try / catch / finally](https://osysharp.com/reference/function/try-catch/)).

A function reached from **outside** the app — over its published REST surface, or as a tool call — is different: a
fault becomes a failure carrying **your message**, and **the type does not survive the crossing**. So write the
message for the person who will read it, and do not expect an external caller to branch on `NotFoundException` versus
`ValidationException`. Inside, the type is everything; at the edge, the message is.

## See also       {#see-also}
- [try / catch / finally](https://osysharp.com/reference/function/try-catch/) — catching one, and what else can throw
- [UnitOfWork](https://osysharp.com/reference/function/unit-of-work/) — `UnitOfWork.Discard()`, the third answer to a refused row
- [function](https://osysharp.com/reference/function/declaration/) — why a fault discards the writes: a function is one transaction
- [constraints](https://osysharp.com/reference/entity/constraints/) · [invariant](https://osysharp.com/reference/entity/invariants/) — the rules that raise `ValidationException` for you
- [Assert](https://osysharp.com/reference/testing/assert/) — `Assert.Throws<T>`, which is how you prove a function refuses what it should


---

<!-- https://osysharp.com/reference/function/try-catch/ -->

# try / catch / finally

> Handles a fault. C#'s syntax, including typed catches, catch filters and finally. The rows written inside a try block that throws are discarded — so a caught fault leaves your data where it was, not half-changed.

<!-- id: function-try-catch · area: function · stability: stable · html: https://osysharp.com/reference/function/try-catch/ -->

## Summary        {#summary}
`try` / `catch` / `finally` is C#'s, and it behaves the way you expect — with one addition that is the reason to reach
for it here: **a `try` block that throws discards the rows it wrote.**

So a caught fault does not leave you holding half-applied changes to clean up. The block either happened or it did
not:

```osy title="the rejected row does not survive the catch" test app=function-try-catch
entity Order {
  [Required, Unique, MaxLength(20)] string Code;
  decimal Total;
  invariant Total >= 0;                       // broken → a catchable ValidationException

  // No `security { }` block means denied to everyone, so every example declares its grants.
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

entity AuditLine {
  [Required, MaxLength(200)] string Message;
  security { allow create, read when IsAuthenticated || IsAnonymous; }
}

string Place(string code, decimal total) {
  try {
    var order = new Order { Code = code, Total = total };
  }
  catch (ValidationException e) {
    var line = new AuditLine { Message = $"rejected {code}: {e.Message}" };
    return "rejected";                // the bad Order is gone; this AuditLine is kept
  }
  return "placed";
}
```

## Signature      {#signature}
```osy syntax
try {
  …
}
catch (<ExceptionType> e) when (<filter>) {   // the type, the name and the `when` are each optional
  …
}
catch {                                       // a bare catch-all
  …
}
finally {                                     // always runs — including on return, break and continue
  …
}
```
A `try` must be followed by a `catch` or a `finally` (or both). A lone `try` is a compile error.

## Description    {#description}

### What you can catch   {#what-throws}
Three different things raise a fault, and only the first is yours:

| What happened | You catch it as |
|---|---|
| **You threw** ([throw](https://osysharp.com/reference/function/throw/)) | the type you threw — or `Exception` |
| **A [[entity-constraints\|constraint]] or [[entity-invariants\|invariant]] was broken** | **`ValidationException`** |
| **A concurrent write collided with yours** | **`ConflictException`** |
| A division by zero, a member read on a null | **only `Exception`** — these have no specific type |

The second row is the one that changes how you write code. A broken invariant is not a database error you have to
recognise by its text — the platform raises it as an ordinary `ValidationException`, in the same closed vocabulary as
the ones you throw yourself. Your validation rules live on the entity, where they are enforced for **every** writer,
and a caller that wants to *handle* a violation rather than fail on it just catches it.

### The rollback is per-block, not per-function   {#rollback}
A function is transactional — a fault it does not catch discards everything it wrote ([function](https://osysharp.com/reference/function/declaration/)). A
`try` block is the same idea, **scoped to the block**:

- The rows written inside the `try` are discarded if it throws.
- The rows written inside the `catch` are kept — the handler is not tarred with the failure it is handling.
- Everything the function wrote **before** the `try` is untouched.

That is what makes the pattern above honest: the rejected `Order` cannot linger, and the `AuditLine` recording the
rejection is still there to read. Proved, not asserted:

```osy title="what survives a caught fault, and what does not" run app=function-try-catch
[Test]
void A_broken_invariant_arrives_as_a_ValidationException() {
  Assert.Equal("rejected", Place("A1", -5m));       // the invariant `Total >= 0` was broken

  Assert.Empty(Order.ToList());                     // the try block's row did NOT survive
  Assert.Single(AuditLine.ToList());                // …and the catch block's row DID
}

[Test]
void An_acceptable_row_is_simply_written() {
  Assert.Equal("placed", Place("A2", 10m));

  Assert.Single(Order.ToList());
  Assert.Empty(AuditLine.ToList());
}
```

### Catching by type, in order   {#by-type}
Clauses are tried **in source order**, and the first one whose type matches wins. `Exception` is the base of all of
them, so a `catch (Exception e)` matches anything — which makes it the last clause you write, never the first:

```osy title="the specific case, then the general one" test app=function-try-catch
string Describe(string code, decimal total) {
  try {
    var order = new Order { Code = code, Total = total };
    return "placed";
  }
  catch (ValidationException e) { return $"invalid: {e.Message}"; }   // the row broke a rule
  catch (ConflictException e)   { return $"conflict: {e.Message}"; }  // someone else got there first
  catch (Exception e)           { return $"failed: {e.Message}"; }    // anything else at all
}
```

The caught value carries a **`Message`** — the text of the throw, or the platform's explanation of the rule you broke
— and a **`Type`**.

### `when` filters a catch   {#when}
A `when` clause decides whether *this* handler is the right one, without catching and rethrowing to find out. The
caught value is in scope inside the filter, so you can look at the message before committing to handle it:

```osy title="handle only the case you know how to handle" test app=function-try-catch
string Refund(string code, decimal amount) {
  try {
    var order = Order.Single(o => o.Code == code);
    if (amount <= 0m) { throw new ValidationException("refund must be a positive amount"); }

    order.Total = order.Total - amount;   // may leave Total negative → breaks the invariant
    return "refunded";
  }
  catch (ValidationException e) when (e.Message.StartsWith("refund")) {
    return "declined";       // MY refusal — the one case this function knows how to answer for
  }
  // the broken invariant is ALSO a ValidationException — but its message is not mine, the filter
  // does not match it, and it keeps travelling out to the caller
}
```

Both faults here are `ValidationException`. The filter is what tells them apart — so filter on a message **you
threw**. The platform writes its own text for a rule it enforces on your behalf, and that text is its to change;
matching on it would couple your control flow to wording you do not own.

An unmatched fault is not swallowed: it travels up to the caller, and if nobody catches it the function faults and
discards its writes.

```osy title="same type, different message — the filter decides" run app=function-try-catch
[Test]
void The_filtered_catch_handles_the_case_it_recognises() {
  Place("A9", 100m);

  Assert.Equal("declined", Refund("A9", -5m));                    // my message → filtered in, handled
  Assert.Equal(100m, Order.Single(o => o.Code == "A9").Total);    // …and the declined refund changed nothing
}

[Test]
void A_fault_the_filter_does_not_match_keeps_travelling() {
  Place("A9", 100m);

  // Refunding 500 from 100 leaves Total at -400, which breaks `invariant Total >= 0`. Same exception TYPE,
  // but not my message — so the `when` skips it, Refund does NOT return "declined", and the fault leaves.
  Assert.Throws<ValidationException>(() => Refund("A9", 500m));

  Assert.Equal(100m, Order.Single(o => o.Code == "A9").Total);    // the try block's write went with the fault
}
```

### `finally` always runs — but it cannot outlive a fault   {#finally}
`finally` runs on the way out however you leave: off the end, on a `return`, and on a `break` or `continue` that
leaves a loop from inside it.

```osy title="finally runs on the way out, however you leave" test app=function-try-catch
string Attempt(string code) {
  try {
    if (code == "") { throw new ValidationException("a code is required"); }
    return "ok";                                             // …the finally still runs on this path
  }
  finally {
    var line = new AuditLine { Message = $"attempted '{code}'" };
  }
}
```

**Now the part that catches people, and it follows from the transaction rather than from `finally`.** If the fault
escapes the function, the function's whole transaction is discarded — and the `finally` block is *part of the
function*, so the rows **it** wrote are discarded too. The block runs; its writes do not survive:

```osy title="what a finally can and cannot leave behind" run app=function-try-catch
[Test]
void On_the_normal_path_the_finally_write_is_kept() {
  Assert.Equal("ok", Attempt("A3"));

  Assert.Single(AuditLine.ToList());   // the function returned, so its transaction committed
}

[Test]
void When_the_fault_escapes_even_the_finally_write_is_discarded() {
  Assert.Throws<ValidationException>(() => Attempt(""));

  Assert.Empty(AuditLine.ToList());    // NOT one row: the fault took the whole function's writes with it
}
```

So **`finally` is not where you record that something failed.** A row written there survives only when the function
goes on to succeed — exactly the case where there was nothing to record.

To persist a record of a failure, **catch it**: a caught fault means the function completes normally, so the `catch`
block's writes commit (which is what the `Place` example at the top of this page relies on). If it must be recorded
even when the fault escapes, use [`Log.Error`](https://osysharp.com/reference/diagnostics/log/) — a log line is not a row, and a rollback cannot take
it back.

### The one thing you cannot catch   {#uncatchable}
A **runaway function** — one that recurses without end, or loops without end — is stopped by the platform, and that
stop is **not catchable**. A `while (true) { try { … } catch { } }` cannot swallow its own kill signal and keep going.

This is deliberate: the limit exists to protect everything else running on the platform, so it cannot be something an
app can opt out of by wrapping it in a handler.

### The caveat: `UnitOfWork.Commit()` is a real write   {#commit-caveat}
Everything above is about rows that have not been persisted yet — the ordinary case, because a server function
persists **when it returns** ([function](https://osysharp.com/reference/function/declaration/)).

If you call `UnitOfWork.Commit()` explicitly in the middle of a function, that is a real write. **A fault afterwards does not
un-write it.** The rollback covers what has accumulated *since* the commit, not what the commit already made durable.
An explicit mid-function `UnitOfWork.Commit()` is therefore a decision to give up all-or-nothing for what came before it — which
is occasionally what you want, and never what you want by accident.

## See also       {#see-also}
- [throw](https://osysharp.com/reference/function/throw/) — raising one, and the closed set of types
- [function](https://osysharp.com/reference/function/declaration/) — the function is the transaction; the `try` block is a smaller one inside it
- [invariant](https://osysharp.com/reference/entity/invariants/) · [constraints](https://osysharp.com/reference/entity/constraints/) — the rules whose violation arrives as `ValidationException`
- [Assert](https://osysharp.com/reference/testing/assert/) — `Assert.Throws<T>`, for proving a function refuses what it should


---

<!-- https://osysharp.com/reference/function/var/ -->

# var

> Declares a local whose type is inferred from its initializer, exactly as in C#. The local is still statically typed — var is about not repeating the type, never about being dynamic.

<!-- id: function-var · area: function · stability: stable · html: https://osysharp.com/reference/function/var/ -->

## Summary        {#summary}
`var` declares a local and infers its type from the initializer — the same `var` as C#. The local is **statically
typed**: `var total = 0m;` is a `decimal` and always will be. `var` saves you writing the type, it does not make the
value dynamic.

## Signature      {#signature}
```osy syntax
var <name> = <expression>;     // the type comes from the expression
```

## Description    {#description}

### It is inference, not dynamism   {#inference}
```osy title="what each var infers" test app=function-var
entity Order {
  [Required] string Code;
  decimal Total;
}

void Locals() {
  var count = 0;                                  // int
  var total = 0m;                                 // decimal — the m suffix matters
  var label = "orders";                           // string
  var order = Order.Single(o => o.Code == "A1");  // Order
  var codes = Order.Where(o => o.Total > 0).ToList();   // Order[]
}
```

Assigning something else later is a compile error, exactly as in C#.

### `var total = 0;` is an int, and it will bite you   {#zero-trap}
The single most common slip. `0` is an `int`, so `var total = 0;` gives you an integer accumulator — and adding
decimals to it will not compile, or worse, will truncate the arithmetic you meant to keep:

```osy title="seed a money accumulator with 0m, not 0" test app=function-var
decimal SumTotals() {
  var total = 0m;                     // decimal — correct
  foreach (var o in Order.Where(o => o.Total > 0).ToList()) {
    total += o.Total;
  }
  return total;
}
```

Write `0m` whenever the accumulator holds money. If you want the type stated outright, use a
[typed local](https://osysharp.com/reference/function/typed-locals/) — `decimal total = 0;` — which says the same thing more loudly.

### When to prefer the explicit type   {#when-explicit}
Use `var` when the initializer already makes the type obvious (`var order = Order.Single(…)`). Write the type out when
it does not, or when the type is the thing the reader needs to know — an accumulator, a boundary, a value someone will
change later.

## See also       {#see-also}
- [Typed locals](https://osysharp.com/reference/function/typed-locals/) — declaring the type explicitly instead
- [const](https://osysharp.com/reference/function/const/) — a local that cannot change
- [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) — why `0` and `0m` are different types


---

<!-- https://osysharp.com/reference/function/verbatim-strings/ -->

# verbatim strings (@"…")

> `@"…"` is a string where a backslash is just a backslash. Nothing inside is an escape, `""` writes a single quote, and the text may run across lines. Use it for regular expressions, paths, and any text where doubling every backslash would make the value harder to read than the thing it describes.

<!-- id: function-verbatim-strings · area: function · stability: stable · html: https://osysharp.com/reference/function/verbatim-strings/ -->

## Summary        {#summary}
An ordinary string treats `\` as the start of an escape: `"\n"` is a newline, `"\t"` a tab. That is what you want
in prose and exactly what you do not want in a regular expression or a path, where a backslash is part of the
value. Prefixing the string with `@` turns escape processing off for the whole literal.

## Signature      {#signature}
```osy syntax
@"ORD-\d{6}"            // a regular expression, written the way a regular expression is written
@"C:\logs\today.txt"    // a path, with single backslashes
@"she said ""no"""      // "" is one literal quote — the only sequence with a meaning
```

## Description    {#description}
Inside `@"…"`:

- **A backslash is a backslash.** `@"\d"` is two characters. The same value written ordinarily is `"\\d"`.
- **`""` is one quote character.** It is the only two-character sequence that means something else, and it exists
  because a lone `"` has to be able to end the literal.
- **A newline is allowed.** The literal runs until its closing quote, so it may span lines, and the line breaks are
  part of the value.

Everything else about the string is unchanged — it is the same type, usable anywhere a string is.

Interpolation is a separate prefix: `$"…"` substitutes `{Holes}`. The two are not combined today; build the value
with an ordinary interpolated string, or keep the pattern verbatim and interpolate around it.

## Examples       {#examples}
```osy title="a pattern that means what it says" test app=verbatim-strings
entity Shipment {
  [MaxLength(64)] string Reference;
}

app.Memory = new MemoryConfig {
  // Verbatim: the regular expression reads as a regular expression.
  Identifiers = [ @"SHP-\d{6}", @"[A-Z]{3}-\d{4}" ]
};
```

```osy title="the same value, both ways" test app=verbatim-strings
string PatternA() { return @"ORD-\d{6}"; }    // verbatim — one backslash
string PatternB() { return "ORD-\\d{6}"; }    // ordinary — the backslash is escaped

// Both return the six characters `ORD-\d{6}`; the first is the one you can read.
```

## See also       {#see-also}
- [identifier patterns (app.Memory)](https://osysharp.com/reference/config/memory/) — the identifier patterns this notation exists to make readable
- [format specifiers](https://osysharp.com/reference/function/format-specifiers/) — `$"…"` interpolation and its `{value:format}` holes


---

<!-- https://osysharp.com/reference/function/while-loop/ -->

# while

> Repeats while a bool condition holds. Reach for it when the number of iterations is not known up front — otherwise a foreach or a for loop says more.

<!-- id: function-while-loop · area: function · stability: stable · html: https://osysharp.com/reference/function/while-loop/ -->

## Summary        {#summary}
`while` repeats its body as long as the condition is true, checked **before** each pass — so a condition that is false
at the start means the body never runs. The condition must be a `bool`.

## Signature      {#signature}
```osy syntax
while (<bool>) { … }
```

## Description    {#description}

### `while`, `foreach`, or `for` — which loop?   {#when}
Use `while` when you do not know up front how many passes you need — consuming until something is exhausted,
converging on a value. When you *are* walking a collection, [`foreach`](https://osysharp.com/reference/function/foreach/) says it better; when you
are counting, so does [`for`](https://osysharp.com/reference/function/for-loop/).

```osy title="halving until it fits" test app=function-while-loop
int TimesToHalve(decimal amount, decimal limit) {
  var steps = 0;
  var current = amount;
  while (current > limit) {
    current = current / 2;
    steps += 1;
  }
  return steps;
}
```

### Advance the condition, or it never ends   {#termination}
The body must move the condition towards false. A `while` whose condition never changes is an infinite loop, and the
platform will not save you from it — it will simply run until it is stopped. Make the thing the condition reads the
thing the body changes, and keep them close enough to see together.

### How do I stop part-way through?   {#leaving}
`break` leaves the loop; `continue` skips to the next check. See [break / continue](https://osysharp.com/reference/function/break-continue/).

```osy title="stopping when you have enough" test app=function-while-loop
int CountUpTo(int limit) {
  var i = 0;
  while (true) {
    i += 1;
    if (i >= limit) { break; }     // the exit is explicit, and easy to find
  }
  return i;
}
```

## See also       {#see-also}
- [foreach](https://osysharp.com/reference/function/foreach/) — walking a collection
- [for](https://osysharp.com/reference/function/for-loop/) — a counted loop
- [break / continue](https://osysharp.com/reference/function/break-continue/) — leaving a loop, or skipping a pass


---

<!-- https://osysharp.com/reference/function/yield/ -->

# yield — a function that produces results over time

> A `stream<T>` function produces its results one at a time instead of all at once, and a `live var` bound to one renders each item the moment it arrives. Use it for anything whose answer builds up rather than appearing — an assistant's reply, a log tail, a long import's progress, a search that finds matches as it goes.

<!-- id: function-yield · area: function · stability: stable · html: https://osysharp.com/reference/function/yield/ -->

## Summary        {#summary}
Most functions answer once: you call them, they think, they return. Some answers do not work that way — an
assistant's reply arrives a word at a time, a log tail never finishes at all, an import of fifty thousand rows has
something useful to say long before it is done.

A **`stream<T>`** function produces its results one at a time:

```osy title="the producer side — a function that answers many times" test app=function-yield-tail
stream<string> Tail(string path) {
  foreach (var line in Lines(path)) {
    yield return line;
  }
}

string[] Lines(string path) { return [path]; }
```

A component binds it with an ordinary `live var`, and renders each item as it arrives:

```osy title="the consumer side — a live var renders items as they land" test app=function-yield-tail
component LogView(string Path) {
  live var lines = Tail(Path);

  render {
    Stack(overflowY: Overflow.Auto, stickToBottom: true) {
      foreach (var line in lines) { Text(line); }
    }
  }
}
```

Nothing polls, nothing re-fetches, and no item is rendered twice.

## Signature      {#signature}
```osy syntax
stream<T> Name(args) { … yield return item; … }   // declare a producer

live var items = Name(args);                      // observe it; items appear as they arrive
foreach (var item in items) { … }                 // renders each one, once
items.Count                                       // how many so far
items.Failed · items.Error                        // it stopped early, and why
items.Interrupted                                 // …and it was the CONNECTION that dropped, so a retry may work
items.Done                                        // the producer finished
```

## Description    {#description}

### What `yield return` does    {#yield}
`yield return x;` hands one item to the caller **and keeps going**. The function does not end — the next statement
runs, and the next `yield return` delivers the next item. When the function reaches its end, the stream is complete.

```osy title="yield return hands one item over and carries on" test app=function-yield-search
entity Document { [MaxLength(200)] string Title; [MaxLength(4000)] string Body; }

class Match { public string Title; public string Snippet; }

stream<Match> Search(string term) {
  foreach (var doc in Document.Where(d => d.Body.Contains(term))) {
    yield return new Match { Title = doc.Title, Snippet = doc.Body };
  }
}
```

To stop early, `return;` on its own — the stream completes normally, with whatever it produced so far.

```osy title="a bare return stops early and completes the stream" test app=function-yield-firstpage
stream<string> FirstPage(string path) {
  var n = 0;
  foreach (var line in Lines(path)) {
    if (n >= 100) { return; }          // enough — complete the stream
    n = n + 1;
    yield return line;
  }
}

string[] Lines(string path) { return [path]; }
```

> **Coming from C#?** There is no `yield break` here. C# needs it because a bare `return;` in an iterator is
> ambiguous with returning a value; a `stream<T>` function never returns a value, so `return;` is unambiguous and
> means exactly what `yield break` means in C#. Everything else is the same, including `yield return` itself.

### A stream is observed, never awaited    {#not-awaitable}
A `stream<T>` can only be bound to a `live var`. Calling one anywhere else is a compile error, because there is no
"the whole thing" to hold — the answer is still arriving:

```osy syntax
var lines = Tail(path);        // ✗ a stream has no single value to assign
on mount { Tail(path); }       // ✗ same reason
live var lines = Tail(path);   // ✓
```

This is the same distinction [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) already draws. A `live var` is a **value binding** — it says what a
value *is*, continuously — and a stream is exactly that: a collection that is still being written.

### Why an ordinary server function cannot be a `live var`    {#why-not-ordinary}
[The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) refuses `live var files = FilesInFolder(id);` because nothing subscribes that value to anything —
it would be fetched once and then quietly go stale, or force a hand-off to the server mid-render.

A stream removes that objection rather than working around it: **the stream itself is the subscription.** The server
holds the connection open and pushes; there is nothing to poll and nothing to invalidate. That is why `stream<T>` is
allowed exactly where an ordinary server call is not.

### Items only ever arrive — they are never revised    {#append-only}
A stream is **append-only**. There is no way to change or remove an item once it has been yielded, and that is a
guarantee rather than a missing feature: it is what producers actually do (an assistant never un-says a word, a log
never un-writes a line), and it is what keeps rendering cheap. Appending touches the end of the list, so the items
already on screen are left alone — see [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/), whose renderer keeps the DOM of every block that did not
change.

If you need to *replace* a value as it evolves, that is an ordinary reactive read, not a stream.

### How does a stream end, and how do I tell which way?    {#completion}
Four ways, and a component can tell them apart:

| | `Done` | `Failed` | `Interrupted` | what to show |
|---|---|---|---|---|
| the function ended | `true` | `false` | `false` | the finished list |
| the function raised an error | `false` | `true` | `false` | the items so far, plus `Error` |
| the connection dropped | `false` | `true` | `true` | the items so far, and an offer to retry |
| still producing | `false` | `false` | `false` | the items so far, and usually a spinner |

**`Interrupted` narrows `Failed`; it does not replace it.** A dropped connection is both, so a component that only
checks `Failed` still shows something — where two mutually exclusive flags would leave it waiting forever on a drop.

**A dropped connection does not stop the producer, and the platform reconnects for you.** The producer's life is its
own: it keeps running on the server while the browser is away, and reconnecting *resumes reading the same run* from
the item you already have. Nothing is re-run, so item 40 is the same item 40 — which is what makes reconnecting
automatic rather than a way to splice the first half of one answer onto the second half of another. A few attempts
are made, backing off; `Interrupted` is what you hear when they are spent, so it means "this is not coming back",
not "the connection blinked".

Offering a retry is therefore about starting *again*, and that is meaningful only when re-running the producer would
produce the same items. A tail of a file or a read of stored rows will; an assistant's reply will not, because a
second run writes a different answer — so an app that streams replies is usually better asking the question again
than resuming it.

**Leaving stops it.** A component that unmounts, or a page that navigates away, tells the server it is going — the
producer stops immediately, rather than running on for the grace window that covers a genuine drop.

**An answer that finished while you were away is kept.** If the connection dropped and the producer went on to
finish with nobody reading, the platform keeps that answer so the reader can still collect it — across a restart, not
only for the few minutes it stays in memory. How long, and how much, are the platform's to decide: there is no
attribute to write and no knob to get wrong.

**A stream that fails keeps everything it already produced.** A reply that broke off halfway still said what it
said, and the reader has already read it — discarding it would destroy the only record of how far it got.

```osy test app=function-yield-answer
stream<string> Ask(string question) { yield return question; }

component Answer(string Question) {
  live var reply = Ask(Question);

  render {
    Stack {
      foreach (var part in reply) { Markdown(part, streaming: !reply.Done); }
      if (reply.Failed) { Text(reply.Error); }
      else if (!reply.Done) { Text("…"); }
    }
  }
}
```

### Where it runs    {#where-it-runs}
`stream<T>` is a **server** producer — that is decided by the declaration itself, not by anything you write — and
its items cross to the browser as they are produced, over the caller's own connection. Items go to the component that asked for them and to nothing else — a stream is never broadcast, and one
visitor's results are never visible to another.

Everything a stream reads obeys the same rules any server read does. There is nothing extra to declare and nothing
extra to check.

### Streaming markdown    {#markdown}
The common case for a text stream is markdown that is still being written, which [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) handles directly:

```osy syntax
live var reply = Ask(question);

render {
  Stack(overflowY: Overflow.Auto, stickToBottom: following) {
    foreach (var part in reply) {
      Markdown(part, streaming: !reply.Done);
    }
  }
}
```

`streaming:` holds a half-typed construct together so the reader never sees raw markdown syntax, and
`stickToBottom:` follows the new text without yanking a reader who has scrolled up ([layout primitives](https://osysharp.com/reference/ui/layout/)).

## See also {#see-also}
- [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) — `live var`, and what may initialize one
- [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) — rendering a stream of markdown as it arrives
- [layout primitives](https://osysharp.com/reference/ui/layout/) — `stickToBottom`, for a surface that grows while you read it


---

<!-- https://osysharp.com/reference/http/index/ -->

# Calling other services

> Two ways to make an outbound HTTP call. A `client` block is a typed wrapper around a known API — name the base URL once, declare each operation as a verb-tagged method. `Http.*` is the facade for a URL you only know at runtime — a webhook, a discovered endpoint. Both hand back an `HttpResponse` you branch on; a non-2xx is a value, not a throw.

<!-- id: http-index · area: http · stability: stable · html: https://osysharp.com/reference/http/index/ -->

## Summary        {#summary}
When your app has to reach *out* to another service — a payment API, a shipping tracker, a webhook — you have two
tools, and which one you pick is decided by a single question: **do you know the API at author time?**

- You know it → a typed [a typed HTTP client (client)](https://osysharp.com/reference/http/client/) block. You declare the shape once and every call site is type-checked.
- You don't (the URL is built at runtime) → the [Http.*](https://osysharp.com/reference/http/facade/) `Http.*`.

Either way the result is an [HttpResponse](https://osysharp.com/reference/http/response/), and a non-2xx status is an ordinary value you branch on — not an
exception you have to catch.

## Description    {#description}

### A typed client for a known API   {#client}
A [a typed HTTP client (client)](https://osysharp.com/reference/http/client/) block is the one to reach for when you're integrating a specific, known API. Inside `client Shipping
{ … }` you name the `BaseUrl` once and declare each operation tagged with its verb (`[Get]`,
`[Post]`, `[Put]`, `[Patch]`, `[Delete]`) and a path — e.g. `[Get("/track/{code}")] TrackResult Track(string
code);`. The request and response bodies are typed, so a call site that passes the wrong shape is a compile error, not
a 4am surprise. It reads like calling a local method; the platform does the HTTP. Full syntax and examples on
[a typed HTTP client (client)](https://osysharp.com/reference/http/client/).

### The facade for a runtime URL   {#facade}
When the URL isn't knowable until runtime — a webhook target stored on a record, an endpoint you just discovered —
use the [Http.*](https://osysharp.com/reference/http/facade/). `Http.Get`/`Post`/`Put`/`Delete` take the URL as a value and return an [HttpResponse](https://osysharp.com/reference/http/response/), so
a webhook call is `var res = Http.Post(webhookUrl, body);` and you branch on `res.IsSuccess`.

### The response is a value, not a throw   {#response}
An [HttpResponse](https://osysharp.com/reference/http/response/) carries the status code, the body as text, and an `IsSuccess` flag (true for 2xx). A non-2xx —
a 404, a 500, a rate-limit 429 — is a normal return you inspect, not an exception. That is deliberate: an outbound
call fails in ordinary, expected ways, and forcing every one through a `try/catch` would be noise. You branch on the
status the same way you'd branch on any other value.

### Credentials belong in secrets   {#secrets}
An API key or client secret an outbound call needs is declared as a [secret](https://osysharp.com/reference/config/secrets/), never inlined. For
signing users in through a third party, or calling an API on a user's behalf, see [OAuth clients (app.OAuthClients)](https://osysharp.com/reference/config/oauth-clients/).

## See also       {#see-also}
- [a typed HTTP client (client)](https://osysharp.com/reference/http/client/) — the typed `client` block and its verb attributes
- [Http.*](https://osysharp.com/reference/http/facade/) — `Http.*` for a URL built at runtime
- [HttpResponse](https://osysharp.com/reference/http/response/) — the status / body / `IsSuccess` result you branch on
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — where an API key lives; [OAuth clients (app.OAuthClients)](https://osysharp.com/reference/config/oauth-clients/) — third-party sign-in and delegated calls


---

<!-- https://osysharp.com/reference/http/facade/ -->

# Http.*

> Make an outbound HTTP call to a URL you build at runtime — a webhook, a third-party API, a discovered endpoint. `Http.Get`/`Post`/`Put`/`Patch`/`Delete` return an `HttpResponse` you branch on (`StatusCode`, `Body`, `IsSuccess`); a 4xx/5xx is data, not an exception. Default-open to public hosts; internal addresses are blocked.

<!-- id: http-facade · area: http · stability: stable · html: https://osysharp.com/reference/http/facade/ -->

## Summary        {#summary}
**`Http.*`** is the outbound-HTTP facade for URLs you can't name at author time — a webhook target, a REST API, an
endpoint that comes from data. It mirrors the shape of a familiar HTTP client:

```osy syntax
use Osysharp.Http;

var r = Http.Get("https://api.example.com/orders/" + order.Code);
if (r.IsSuccess) {
  order.Tracking = r.Body;
}
```

Every verb returns an [HttpResponse](https://osysharp.com/reference/http/response/) (`StatusCode`, `Body`, `IsSuccess`). A non-2xx status is a **normal return**,
not an error — a `404` gives you `r.StatusCode == 404` and `r.IsSuccess == false`, so you branch on the result instead
of catching an exception.

For a **declared, fixed** endpoint with typed request/response, use a `client` block instead — `Http.*` is the
escape hatch for the dynamic case beside it.

## Signature      {#signature}
```osy syntax
use Osysharp.Http;

HttpResponse Http.Get(string url [, Map<string, string> headers])
HttpResponse Http.Delete(string url [, Map<string, string> headers])
HttpResponse Http.Post(string url, string|byte[] body, string contentType [, Map<string, string> headers])
HttpResponse Http.Put(string url, string|byte[] body, string contentType [, Map<string, string> headers])
HttpResponse Http.Patch(string url, string|byte[] body, string contentType [, Map<string, string> headers])
```

- **`url`** — an absolute `http`/`https` URL.
- **`body`** / **`contentType`** (Post/Put/Patch) — the request body and its media type (e.g. `"application/json"`).
  The body may be a **`string`** or a **`byte[]`**; the argument's type decides how it goes on the wire. Bytes go as
  bytes — pushing them through a string would UTF-8-encode them, which silently corrupts anything that is not text.
- **`headers`** (optional, any verb) — extra request headers such as `Authorization`. A `Map<string, string>`.

## Description    {#description}
`Http.*` is enabled by declaring the dependency in your app manifest:

```osy title="the manifest declaration that enables Http.*" syntax
app Shop {
  model "model/**/*.osy";
  use Osysharp.Http;
}
```

An `Http.*` call without `use Osysharp.Http;` is a compile error naming the fix — a network dependency is visible in the
manifest, not hidden in a function body.

**Default open, host-protected.** You can reach any **public** host — that's your call, the same as any dependency.
What the platform guarantees is that you **cannot** reach its own internals: a URL that resolves to a loopback,
private (`10.x`/`192.168.x`/…), or cloud-metadata address is refused — checked against the *resolved* address, so a
hostname that points at an internal IP is blocked too. Two more limits protect the host: an absolute **timeout ceiling**
and a **maximum response size**; a call that runs too long is cancelled and an over-size response is refused.

**Headers and auth.** Pass a headers map to authenticate an outbound call or set a custom content type:

```osy title="authenticating an outbound call with a headers map" syntax
var headers = new Dictionary<string, string>();
headers.Add("Authorization", "Bearer " + token);
var r = Http.Post("https://hooks.example.com/notify", payload, "application/json", headers);
```

**Bodies are text.** The request `body` and the response `Body` are strings. Build or parse JSON with the JSON surface
(paired with this facade) — `Http.*` moves the bytes; it doesn't assume a format.

**Long-running callbacks aren't held connections.** If an external system calls you back minutes or hours later, model
that as a workflow event (a webhook that raises an event), not an `Http.*` call that blocks — the timeout ceiling
exists precisely so a call can't hold a connection open indefinitely.

## Examples       {#examples}

Post a JSON webhook and record whether it was accepted:

```osy title="post a JSON webhook" test app=http-facade
// `use` is a MANIFEST declaration — it belongs in your app.osy, not in a model file.
app Shop {
  model "model/**/*.osy";
  use Osysharp.Http;
}

entity Order {
  [Required] string Code;
  bool WebhookAccepted;
  int WebhookStatus;
}

void NotifyShipped(Order order) {
  var body = "{\"order\":\"" + order.Code + "\",\"status\":\"shipped\"}";
  var r = Http.Post("https://hooks.partner.com/orders", body, "application/json");
  order.WebhookAccepted = r.IsSuccess;
  order.WebhookStatus = r.StatusCode;
}
```

Call an authenticated API and use the response body:

```osy title="send request headers, and read the response body" test app=http-facade
string LookupTracking(string carrier, string code, string token) {
  var headers = new Dictionary<string, string>();
  headers.Add("Authorization", "Bearer " + token);
  var r = Http.Get("https://api." + carrier + ".com/track/" + code, headers);
  return r.IsSuccess ? r.Body : "";
}
```

## See also       {#see-also}
- [HttpResponse](https://osysharp.com/reference/http/response/) — the `StatusCode` / `Body` / `IsSuccess` result every verb returns
- [Outbound calls in a test](https://osysharp.com/reference/testing/outbound-calls/) — a `[Test]` makes the call for real; `Http.<Verb>.Stub(url => …)` answers it instead
- [File.Url](https://osysharp.com/reference/storage/file-url/) — the other capability-gated I/O surface (`use Osysharp.Storage;`)


---

<!-- https://osysharp.com/reference/http/response/ -->

# HttpResponse

> The result of an `Http.*` call. Carries the HTTP status code, the body as text AND as bytes, every response header, and a convenience `IsSuccess` flag for 2xx. A non-2xx status is a normal value here, not an exception — you branch on it.

<!-- id: http-response · area: http · stability: stable · html: https://osysharp.com/reference/http/response/ -->

## Summary        {#summary}
**`HttpResponse`** is what every `Http.*` verb ([Http.*](https://osysharp.com/reference/http/facade/)) returns. It has three fields:

| Field | Type | Meaning |
|---|---|---|
| `StatusCode` | `int` | The HTTP status code — `200`, `404`, `500`, … |
| `Body` | `string` | The response body decoded as UTF-8 text |
| `Bytes` | `byte[]` | The body **as it arrived** — the honest answer for anything that is not text |
| `Headers` | `List<HttpHeader>` | Every response header, each with a `Name` and a `Value` |
| `IsSuccess` | `bool` | `true` when the status is in the 2xx range |

A non-2xx response is a **normal return**, so you inspect it rather than catch an error:

```osy syntax
use Osysharp.Http;

var r = Http.Get(url);
if (r.IsSuccess) {
  Process(r.Body);
} else {
  Log("fetch failed with " + r.StatusCode);
}
```

## Signature      {#signature}
```osy syntax
class HttpResponse {
  int StatusCode;
  string Body;                   // decoded as UTF-8
  byte[] Bytes;                  // as it arrived
  List<HttpHeader> Headers;      // each has .Name and .Value
  bool IsSuccess;
}
```

The type enters scope with the same dependency that enables the facade — `use Osysharp.Http;`. You rarely name it
explicitly: `var r = Http.Get(url);` infers it.

## Description    {#description}
`Body` is the response **decoded as UTF-8**. That is right for JSON and wrong for everything else: an image, a PDF,
an object out of a blob store is not text, and decoding it produces mojibake rather than an error. **Read `Bytes`
for anything that is not text** — it is the body exactly as it arrived, and `.Length` tells you how much of it there
is.

### Reading a header      {#headers}
`Headers` is a **list**, not a map, because HTTP headers repeat — `Set-Cookie` is the everyday case, and a map would
silently keep one of them. Each entry has a `Name` and a `Value`:

```osy syntax
var etag = "";
foreach (var h in r.Headers) {
  if (h.Name == "ETag") { etag = h.Value; }
}
```

`ETag` is what verifies a store's own write; `Content-Type` and `Content-Length` are the other two anything binary
usually wants.

`IsSuccess` is exactly `200 ≤ StatusCode < 300`. It's a convenience for the common "did it work?" branch; when you
care about a specific code (a `429` to back off, a `404` to treat as absent), read `StatusCode` directly.

## Examples       {#examples}

Distinguish "not found" from a real failure:

```osy title="a 404 is data, not a failure" test app=http-response
app Shop {
  model "model/**/*.osy";
  use Osysharp.Http;       // the manifest dependency that makes `Http.*` available
}

string FetchOrEmpty(string url) {
  var r = Http.Get(url);
  if (r.StatusCode == 404) return "";        // absent — expected
  if (!r.IsSuccess) return "";                // some other failure
  return r.Body;
}
```

## See also       {#see-also}
- [Http.*](https://osysharp.com/reference/http/facade/) — the `Http.Get`/`Post`/`Put`/`Delete` verbs that return this type


---

<!-- https://osysharp.com/reference/http/client/ -->

# a typed HTTP client (client)

> A `client` block declares a typed wrapper around an external HTTP API: name the BaseUrl once, then declare each operation tagged with its verb — [Get], [Post], [Put], [Patch], [Delete] — and a path. Path parameters bind by name; [Query] and [Header] bind a parameter to the query string or a header; [ResponsePath] unwraps a nested field from the JSON response. You call the operation; the platform makes the request.

<!-- id: http-client · area: http · stability: stable · html: https://osysharp.com/reference/http/client/ -->

## Summary        {#summary}
A `client` block is a **typed wrapper around an external HTTP API**. You declare the base URL once and then, for each
endpoint, an operation tagged with its HTTP verb and path. Calling the operation makes the request and returns
the typed result — you never build a URL or parse a response by hand. It is the declarative counterpart to the
imperative [`Http.*`](https://osysharp.com/reference/http/facade/) facade: reach for a `client` when you call the *same* API repeatedly.

## Signature      {#signature}
```osy syntax
client <Name> {
  BaseUrl = "<https://…>";                 // required: the API's root
  // optional: Timeout, Auth, Retry, Headers …

  [Get("/path/{param}")]                   // the verb + path; {param} binds to a same-named argument
  <Result> <Op>(<params>);

  [Post("/path"), ResponsePath("data")]    // ResponsePath unwraps a nested JSON field
  <Result> <Op>(<Body> body, [Query] string q, [Header] string h);
}
```

## Description    {#description}

### Naming the verb and path — `[Get("/users/{id}")]`   {#verbs}
Each operation carries exactly one verb attribute naming its method and path: **`[Get]`**, **`[Post]`**, **`[Put]`**,
**`[Patch]`**, or **`[Delete]`** — e.g. `[Get("/users/{id}")]`. A `{name}` segment in the path is a **path parameter**:
it binds to the operation argument of the same name, so `[Get("/track/{code}")]` fills `{code}` from the `code`
argument.

### Where does each argument go — query, header, body?   {#params}
An argument that is not a path parameter is bound by an attribute on it:

- **`[Query]`** binds the argument to a **query-string** value: `[Query] string pageToken` becomes `?pageToken=…`.
- **`[Header]`** binds it to a **request header**.
- An un-attributed argument on a `[Post]`/`[Put]`/`[Patch]` is the **request body** — serialized as JSON.

### Unwrapping the response with `[ResponsePath]`   {#responsepath}
Many APIs wrap the payload you want in an envelope — `{ "data": { … } }` or `{ "messages": [ … ] }`. **`[ResponsePath]`**
names the field to unwrap, so the operation returns just that part already typed: `[Get("/messages"),
ResponsePath("messages")] Message[] List();` hands you the array, not the envelope.

### Timeout, auth, retry and headers for the whole block   {#settings}
Beyond `BaseUrl`, a `client` may set a `Timeout`, an `Auth` (e.g. an API key drawn from a [`Secret`](https://osysharp.com/reference/config/secrets/)),
a `Retry` policy, and default `Headers` sent on every request. These are declared once at the top of the block and
apply to every operation.

You just **call** an operation — there is no `async` and no `await` ([async / await — why Osy# has neither](https://osysharp.com/reference/function/async-await/)). Like any effect, the
platform makes the request in place and resumes your function with the result; the suspension is the engine's business,
not something the signature or the call site has to spell.

### Generating a client from an OpenAPI spec   {#generate}
You rarely hand-write a `client` for a large API — **generate it**. `osy import-api <spec>` reads an OpenAPI/Swagger
document (a local file, a URL, or a GitHub blob URL) and writes an `.osy` file containing the whole `client` block: one
operation per endpoint — already tagged with its verb, path, and `[Query]`/`[Header]`/`[ResponsePath]` bindings — plus
the request/response `class` types and any `enum`s, and an `Auth` block wired to a [`Secret`](https://osysharp.com/reference/config/secrets/). Import
just the operations you need with `--tag`, `--filter`, or `--select`; name the auth secret with `--secret` and its
method with `--auth` (`bearer`, `apiKey-header`, `apiKey-query`). The output is ordinary source — review it, trim it,
and commit it like any other `client`.

```bash title="generate a typed email client from a spec, just the email operations"
osy import-api ./resend-openapi.yaml --tag Emails --auth bearer --secret ResendApiKey -o resend.osy
# → resend.osy: `client Resend { BaseUrl = "…"; Auth = new BearerAuth { Secret = Secret.ResendApiKey }; … }`
#   plus the request/response classes. It reminds you to declare the secret once in your app:
#     app.Secrets = [ new Secret("ResendApiKey") ];   // value injected out-of-band, never committed
```

Use `--list` to see the available operations and tags before importing, and `--json` to preview the source it *would*
generate without writing a file.

## Examples       {#examples}
```osy title="a typed client for an external tracking API" test app=http-client
class TrackResult {
  string Status;
  string Location;
}

client Tracking {
  BaseUrl = "https://api.tracking.example";

  // GET /track/{code}?carrier=…  with an API key header; unwrap the "data" envelope.
  [Get("/track/{code}"), ResponsePath("data")]
  TrackResult Track(string code, [Query] string carrier, [Header] string apiKey);
}
```

```osy title="a POST whose body is serialized as JSON" test app=http-client
class ShipmentRequest {
  string OrderCode;
  string Address;
}

class ShipmentResult {
  string TrackingNumber;
}

client Shipping {
  BaseUrl = "https://api.ship.example";

  // The un-attributed `body` argument is the JSON request body.
  [Post("/shipments"), ResponsePath("shipment")]
  ShipmentResult CreateShipment(ShipmentRequest body);
}
```

## See also       {#see-also}
- [Http.*](https://osysharp.com/reference/http/facade/) — the imperative `Http.*` facade, for a one-off call rather than a reused API
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — where a client's API key comes from (`Secret.Name`)
- [publishing a REST API (app.Apis)](https://osysharp.com/reference/api/rest/) — the other direction: publishing *your* app as a REST API
- [async / await — why Osy# has neither](https://osysharp.com/reference/function/async-await/) — why there is no `async`; you just call the operation


---

<!-- https://osysharp.com/reference/json/serializer/ -->

# JsonSerializer

> Turn a value into a JSON string and a JSON string into a typed object — the C#-faithful System.Text.Json spelling. `JsonSerializer.Serialize(order)` gives the JSON text; `JsonSerializer.Deserialize<Order>(body)` parses it back into a typed `Order`. Pure — no capability needed.

<!-- id: json-serializer · area: json · stability: stable · html: https://osysharp.com/reference/json/serializer/ -->

## Summary        {#summary}
**`JsonSerializer`** is the JSON surface, spelled exactly as in C# (`System.Text.Json`). It has two members:

```osy syntax
var body = JsonSerializer.Serialize(order);              // a value → a JSON string
var order = JsonSerializer.Deserialize<Order>(body);     // a JSON string → a typed Order
```

It is **pure** — no capability, no `using` required (though a pasted `using System.Text.Json;` is accepted and does
nothing). It pairs naturally with [Http.*](https://osysharp.com/reference/http/facade/): serialize a request body, deserialize a response.

## Signature      {#signature}
```osy syntax
string JsonSerializer.Serialize(value)          // value = a scalar, a class, a list, or a map
T      JsonSerializer.Deserialize<T>(string json) // T = a class
```

## Description    {#description}
**`Serialize`** accepts any value and returns compact JSON (matching System.Text.Json's default): a scalar
(`"x"`, `42`, `true`, `3.5`), a **class** instance (→ a JSON object of its fields), a **list** (→ an array), a **map**
(→ an object). Property names are the field names as declared. `byte[]` becomes base64.

**`Deserialize<T>`** parses JSON into a `T`, where **`T` is a `class`** — the DTO you want the data as. It fills each
declared field from the matching JSON member, coercing by the field's type: scalars, a nested class (→ a nested
object), and a collection (→ a list of elements). A member missing from the JSON is left at its default; a JSON `null`
sets null. A field the class doesn't declare is ignored.

`T` must be a **class**, not an `entity` — an entity has identity and persistence a JSON body can't carry.

### Storing it: the `Json` property type     {#json-column}
A property declared `Json` holds a document, and it is **string-backed** — so `Serialize` writes one and the property
reads back into `Deserialize`:

```osy syntax
class Detail { public int Attempt; public string Reason; }

entity Job { [Required, MaxLength(50)] string Name; Json Detail; }

void Record() {
  var j = new Job { Name = "j1", Detail = JsonSerializer.Serialize(new Detail { Attempt = 2, Reason = "retry" }) };
}

int Attempts(Job job) {
  return JsonSerializer.Deserialize<Detail>(job.Detail).Attempt;
}
```

That is the whole surface: a `Json` property takes a string and gives one back. It is **not parsed or validated** on
the way in — the same rule a `Markdown` property follows — so `Serialize` is how you sensibly produce one rather than
building the text by hand.

⚠ There is no object-literal form: `Detail = new { attempt = 2 }` does not compile, because an anonymous object is
not a value in Osy#. Declare the shape as a `class` and serialize it, which is what you would do in C# anyway and
gives the document a name the rest of your code can use.

**Serializing an `entity`** produces a **shallow** object: its `Id` and scalar properties, with an `EntityRef`
rendered as its FK id (not the nested entity) and collections omitted. This is deliberate — expanding relations by
default would invite reference cycles and load a record's whole object graph. When you want a specific nested shape,
map the entity into a `class` DTO (which serializes fully) and serialize that.

### How deep may a value nest, and what about a cycle?     {#depth-and-cycles}
Nesting is capped at **500 levels** — a class whose field is a class whose field is a class, a list of lists, a map
of maps. Past that, `Serialize` refuses with an error naming the depth, the limit and the class it stopped inside.
The cap is on nesting only: a list of a million flat items is depth 2 and serializes fine.

A value that **refers back to itself** is refused too, with its own message rather than a depth one — JSON has no way
to write a reference to a value it has already written, so there is nothing faithful to produce:

```text
JsonSerializer.Serialize cannot serialize an instance of class 'Node', because it refers back to itself — JSON has
no way to write a reference to a value it has already written. Break the cycle before serializing (drop the
back-pointer, or serialize the id instead of the object).
```

The same value reached twice down **different** branches is not a cycle: it is written out once per occurrence, as
you would expect. See [How deep can an object graph get?](https://osysharp.com/reference/function/deep-object-graphs/) for how the limit is counted and why it exists.

## Examples       {#examples}

Round-trip a DTO through JSON:

```osy title="round-trip a DTO" test app=json-serializer
class Order {
  public string Code;
  public decimal Total;
  public bool Paid;
}

string ToJson(Order o) {
  return JsonSerializer.Serialize(o);              // {"Code":"A1","Total":42.0,"Paid":true}
}

Order FromJson(string body) {
  return JsonSerializer.Deserialize<Order>(body);  // typed Order, fields populated
}
```

Parse an HTTP response body ([Http.*](https://osysharp.com/reference/http/facade/)):

```osy title="parse an HTTP response body" test app=json-serializer
app Shop {
  model "model/**/*.osy";
  use Osysharp.Http;       // `use` is a manifest declaration — Http.* needs it
}

class Weather { public decimal TempC; public string Summary; }

Weather Fetch(string city) {
  var r = Http.Get("https://api.example.com/weather/" + city);
  return r.IsSuccess ? JsonSerializer.Deserialize<Weather>(r.Body) : new Weather { Summary = "unknown" };
}
```

Nested classes and lists deserialize recursively:

```osy title="nested classes and lists" test app=json-serializer
class Line { public string Sku; public int Qty; }
class Cart { public string Owner; public List<Line> Lines; }

Cart Parse(string body) {
  return JsonSerializer.Deserialize<Cart>(body);   // Lines becomes a list of typed Line objects
}
```

## See also       {#see-also}
- [Json](https://osysharp.com/reference/types/json/) — the `Json` property type this writes into, and when to reach for it
- [Http.*](https://osysharp.com/reference/http/facade/) — the outbound HTTP surface whose bodies this serializes / parses
- [constructor](https://osysharp.com/reference/class/constructors/) — the `class` types Serialize walks and Deserialize targets
- [How deep can an object graph get?](https://osysharp.com/reference/function/deep-object-graphs/) — how deep a value may nest before Serialize refuses it, and what happens to a cycle


---

<!-- https://osysharp.com/reference/local/adding-an-account/ -->

# Adding an account

> Adds a user to your app on the local platform, with the roles you say it should have. Use it to give yourself an account to log in with, and to create the privileged user that `osy run --as` and `osy import --as` act as.

<!-- id: local-adding-an-account · area: local · stability: stable · html: https://osysharp.com/reference/local/adding-an-account/ -->

## Summary        {#summary}

Adds an account to your app on the local platform: a row in your app's own user entity, with a hashed password and
the roles you asked for. It is how you get something to log in as, and how you create the user that `--as` names.

**This is the way to seed an app's accounts** — including the several users in different roles a test or a demo needs
in order to show that different people see different things. The remote twin is `osyrin app user add`: same act, same
store, same flags.

The account is **your app's**, not the platform's. The login field, the password field and the role vocabulary all
come from what your app declares — so the account you add is exactly the kind of account your app's own login page
produces, and it is subject to the same rules afterwards.

## Signature      {#signature}

```console
osy user add <login> [path] --role <name>… [--set <field>=<value>]… [--grant <field>=<value>]… [--grant-entity <name>] [--password <pw>] [--via-signup] [--devname <name>]
```

## Description    {#description}

### What it writes   {#what}

Your app declares its login this way:

```osy title="the declaration the command reads" test app=local-adding-an-account
[Principal] entity User {
  [Required, MaxLength(200), Unique] string Email;
  [MaxLength(200)] [Required] string PasswordHash;
  security {
    allow read when IsAuthenticated;
    // The platform ships the [Principal] to the client for `Session.CurrentUser.*` — minus its MASKED properties.
    // Without this line the hash rides that payload to the browser. See [app.Auth — how the platform authenticates a user of your app](https://osysharp.com/reference/security/password-auth/) for why the
    // condition is `when IsAuthenticated` here: sign-in happens while you are still anonymous.
    deny read PasswordHash when IsAuthenticated;
  }
}

app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };
```

`osy user add ops@example.com` writes one `User` row: `Email` set to the login you gave, `PasswordHash` set to a
hash of the password. Nothing is hardcoded — rename the entity or the fields and the command follows, because it
reads the same declaration your login page does.

Omit `--password` and you are prompted for it, and the prompt does not echo.

### Giving it roles   {#roles}

An account you add by hand usually exists *because* it needs authority, so **`--role` is required** — once per role:

```console
$ osy user add ops@example.com --password 's3cret' --role Admin --role Support
✓ Added ops@example.com to Acme Ops with Admin, Support.
```

The names come from your app's own role vocabulary, and the grant lands in the entity that pairs a user with it:

```osy title="the role vocabulary and where a grant lands" test app=local-adding-an-account-roles
[Principal] entity User {
  [Required, MaxLength(200), Unique] string Email;
  [MaxLength(200)] [Required] string PasswordHash;
  security {
    allow read when IsAuthenticated;
    deny read PasswordHash when IsAuthenticated;   // never to the browser — see above
  }
}

[Role] enum AppRole { Admin, Support }

entity RoleGrant {
  [Required] User User;
  [Required] AppRole Role;
}

app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };
```

**A role your app cannot grant refuses the whole command**, and says which of the three things is missing: no `[Role]`
enum at all, no entity pairing a user with one, or a name that is not a member of the enum — that last one with a
suggestion:

```console
$ osy user add ops@example.com --role Admn
✗ 'Admn' is not a member of this application's `[Role]` enum AppRole. Did you mean 'Admin'?
```

Nothing is written when it refuses. An account created with fewer rights than you asked for is worse than none: it
logs in, and then fails at the first page that matters, in a way that reads like a bug in your app.

**Omitting `--role` is an error, not a default.** An account with no role holds nothing your app's `security {}`
can act on, and a missing flag is far more often forgotten than meant:

```console
$ osy user add ops@example.com
✗ `--role` is required: an account with no role holds no authority this app's `security {}` can act on… Pass
  `--role <name>` (repeatable) naming a member of the app's `[Role]` enum — or `--via-signup` to run the app's own
  signup instead, which decides its own grants.
```

If your app declares **more than one** entity that grants roles — say a global one beside a per-project one — a role
name on its own does not say which grant to write, and the command says so and names the candidates. Create the row
you meant directly.

### Setting the other fields on the account   {#set}

The login and the password are the only fields the command knows by itself — they are the two your `app.Auth` names.
Everything else your account entity holds, you say:

```console
$ osy user add lena@acme.test --password 's3cret' --role Member \
    --set Name=Lena --set Department=Legal --set Seniority=4
✓ Added lena@acme.test to Acme Ops with Member.
```

`--set` is repeatable, once per field. Values are written in the type the field declares, using the same rules a
[data file](https://osysharp.com/reference/local/importing-data/) uses — so an **enum is written by member name** (`Department=Legal`, not a number),
a number may be typed as text, and a `true`/`false` lands as a boolean. Only the first `=` splits, so a value may
contain more of them.

**A field it cannot honour refuses the whole command**, and nothing is written:

```console
$ osy user add lena@acme.test --role Member --set Nmae=Lena
✗ 'User' has no property 'Nmae'. Did you mean 'Name'?

$ osy user add lena@acme.test --role Member --set Department=Marketing
✗ 'Department' is a Dept, which has no member 'Marketing'. It declares: Legal, Finance.
```

That is the point of refusing rather than skipping: an account created with a field silently empty looks exactly like
one where the model has no such field, and you find out later, from a blank column.

⚠ **You cannot `--set` the login or the password field.** The login comes from the argument, so passing it too would
be two answers to one question; and the password field holds a **hash**, so setting it as text would write a value no
login could ever verify. Use `--password`, which hashes it.

⚑ **A required field is a required field.** If your account entity requires something beyond the credential — a name,
a department — the command refuses until you `--set` it, and says so. It does not invent a value.

### Seating an admin of ONE organisation   {#grant}

`--role` fills the grant row's role; the account fills its user. In a multi-tenant app the grant row has a third
column — the organisation the role is scoped to — and `--role` alone cannot name it. `--grant` sets the grant row's
other columns, and a reference is resolved by the target's `[Unique]` key or its id:

```console
$ osy user add ops@acme.test --password 's3cret' --role Admin --grant Organization=acme --grant-entity RoleGrant
✓ Added ops@acme.test to SpendFlow with Admin (Organization=acme).
```

`acme` is looked up on `Organization`'s `[Unique]` string columns (`Slug`, `Code`, `Name` — whichever the entity
declares); an id works too. **A tenant that does not exist refuses the whole command** — no admin of nowhere:

```console
$ osy user add ops@acme.test --role Admin --grant Organization=nowhere --grant-entity RoleGrant
✗ no Organization whose Slug is 'nowhere'. Create it first, or pass the row's id.
```

**`--grant-entity` picks the grant table when the app has more than one.** An app that keeps `Membership` and
`RoleGrant` side by side — both a user reference beside a `[Role]` value — is an ordinary shape, and the platform
cannot guess which one an unqualified role means, so it asks:

```console
$ osy user add ops@acme.test --role Admin
✗ this application grants roles through more than one entity (Membership, RoleGrant), so a role name on its own does
  not say which grant to write. Say which with `--grant-entity <name>` (one of: Membership, RoleGrant); the grant
  row's other columns, such as the organisation a role is scoped to, are set with `--grant <field>=<value>`.
```

⚑ **An entity whose only user reference is the ACTOR is not a grant table**, however much it looks like one:
`Invitation { Organization; Email; OrgRole Role; User InvitedBy; }` carries a user reference beside a role, but
`InvitedBy` is the inviter — nobody is granted anything by an invitation. The command never lists it, and the role
resolver never reads it (the rule is in [[security-role-grants#by-shape]]).

### When your app's signup owns its authorization   {#via-signup}

Some apps decide authority **inside their own signup**: the first account to register writes itself an admin grant,
and the app's bootstrap gate is "no users exist yet". For those, an account added generically logs in and is then
refused every admin page — and the row it wrote has closed the bootstrap gate for good.

`--via-signup` runs your app's own declared signup instead of writing the row:

```console
$ osy user add founder@example.com --via-signup
✓ Ran Acme Ops's own signup for founder@example.com.
```

What that grants is entirely your app's decision — which is the point of routing through its function rather than
imitating it. `--role` cannot be combined with `--via-signup` for the same reason: the signup decides what it grants,
so there is nowhere for a role you named to go.

### The first account closes a `Count() == 0` bootstrap gate   {#bootstrap-gate}

This belongs to the act of adding an account, not to any one way of doing it. If your app gates its first-admin path
on `User.Count() == 0`, that gate is closed by the **first row added by any means** — this command, `osyrin app user
add`, `osy import`, a browser signup. If you want the app's own bootstrap to run, run it first (`--via-signup`, or
the signup page) and seed the rest afterwards.

### The account it makes is a real one   {#real}

There is no back door here. The row goes through your app's own declared fields, the password is hashed the same way
a login checks it, and the roles are the app's own. So the account works in the browser, in `osy run --as`, and in
`osy import --as` identically — and an account that cannot do something is telling you your app's rules say so.

## Examples       {#examples}

```console
osy user add me@example.com --role Admin                       # prompt for the password
osy user add ops@example.com --password 's3cret' --role Admin  # the privileged user `--as` names
osy user add lee@example.com --role Legal --role Support       # repeat for several roles
osy user add lena@example.com --role Member --set Name=Lena --set Department=Legal   # the rest of the person
osy user add ops@example.com --role Admin --grant Organization=acme --grant-entity RoleGrant   # admin of ONE tenant
osy user add founder@example.com --via-signup                  # let the app's own signup decide
osy user add me@example.com --role Admin --devname staging     # against a named local instance
```

### Against a deployed app   {#remote}

`osyrin app user add` is the same command against a platform you are logged in to — same act, same store (your app's
own account rows), same flags, same refusals. The two are one verb with two reaches, so anything above holds there.

## See also       {#see-also}

[Running a function](https://osysharp.com/reference/local/running-a-function/) — `osy run --as` names the account you just added.

[Importing data](https://osysharp.com/reference/local/importing-data/) — `osy import --as` does too, for anything the app gates.

[Launching your app](https://osysharp.com/reference/local/launching-your-app/) — open the app in a browser and log in as it.


---

<!-- https://osysharp.com/reference/local/checking-your-app/ -->

# Checking your app

> Checks your app against production best-practice rules and reports where it falls short — the maturity signal, beside the correctness ones. Covers security, the tests that prove it, the data model, the cost of your queries, and what happens when a remote call fails.

<!-- id: local-checking-your-app · area: local · stability: preview · html: https://osysharp.com/reference/local/checking-your-app/ -->

## Summary        {#summary}

Reports where your app falls short of a production-grade bar. Compiling tells you the app is *correct*; `osy lint`
tells you whether it is *finished* — starting with the category that hurts most when it is not: security.

## Signature      {#signature}

```console
osy lint [path] [--json] [--strict]
```

## Description    {#description}

Findings come in three tiers:

- **MUST** — a production app is broken or exposed without it. Every rule at this tier **compiles and type-checks** —
  which is exactly why they need a linter. Most are security; the other is a number that comes back wrong.

  - **A total that is quietly short.** A function creates rows, does not commit, and then sums, averages or takes the
    min or max over that entity ([Sum / Average / Min / Max / Count](https://osysharp.com/reference/query/aggregates/)). The database computes the aggregate over **committed rows
    only**, so the answer silently omits the rows just created — it is short by exactly the work the function just did.
    Nothing fails: the code compiles, runs, and hands back a wrong number. Call `UnitOfWork.Commit();` before the aggregate.
  - **A query that filters on an edit you have not committed.** Change a property on a row, do not commit, then query
    that entity with a filter on the property you changed. The filter runs **in the database, against the committed
    value** — the one from before your edit. So the query misses the row your edit would now match, *and* still
    returns the row it no longer matches; and that row then reads back with your new value, contradicting the very
    filter that selected it. Commit before the query, or filter the rows you already hold in memory. (Reading the
    value straight off the row you edited is fine — that always shows your edit. It is only the *filter* that is
    computed on the old value, and only for a property you actually changed.)

  - **The login nobody tests.** The app has an `[AuthMethod]` and no test proves it **both ways** — that a right
    credential is accepted *and* that a wrong one is refused. Every access rule you wrote sits behind this one
    function, and it is the one that fails invisibly: a login which handed a ticket to anybody passes a suite that
    only signs in successfully, and the app behaves exactly as it does now until the wrong person is holding a
    ticket. Test the refusal too — and include an address with **no account**, which must fail exactly like a wrong
    password, or the form tells an attacker which addresses are registered.
  - **A credential is handed out.** An entity grants reads and a sensitive field (`PasswordHash`, `*Token`, `*Secret`)
    has no field-level `deny read`, so it goes to everyone who can read the row.
  - **A role grant can be written by its own subject.** A [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) table whose writes are ungated — or
    guarded by a `where` row-filter, which here says *"you may write your own privileges"* — lets a caller hand
    themselves whatever role the rest of the app trusts. Every other rule you wrote is then decoration. The same hole
    is reported when someone may `update` or `delete` a grant they could not have `create`d: editing a `Member` grant
    into an `Admin` one is the identical escalation through another verb.
  - **The auth flow is denied its own credential.** An `[AuthMethod]` runs as the ephemeral auth principal, which bears
    a role and has **no user** — working out the user is what it was called to do. An entity it must read, granted only
    by a `where` row-filter, therefore denies it: there is no user id to match, so the login cannot read the record it
    exists to check and fails every time. Your tests still pass, because they run as a real signed-in user for whom
    that filter works perfectly ([auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/)).
  - **A secret is compared with `==`.** `storedHash == Security.HashPassword(password)` is **not merely insecure — it
    can never be true**, because a hash is salted; the login cannot succeed (use `Security.VerifyPassword`,
    [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/)). And `==` on an HMAC tag leaks, through how long the check takes to fail, how much of a guess
    was right — enough to forge one (use `Crypto.FixedTimeEquals`, [Crypto.HmacSha256Hex and Crypto.FixedTimeEquals](https://osysharp.com/reference/function/crypto-hmac/)).
  - **An integer-only format on a Double.** `someDouble.ToString("X")` or `"D"` — those specifiers are integer-only, so
    .NET throws a `FormatException` at runtime. It compiles (`ToString` takes any string), so only the crash tells you.
    Format the Double with `N`/`F`/`C`/`P`, or convert to an integer type first.

- **SHOULD** — expected, and worth flagging.
  - **A login that says WHICH half of the credential was wrong.** Answering "no account with that address" and
    "wrong password" differently lets anybody discover which addresses are registered, without ever guessing a
    password — the input to credential-stuffing and to targeted phishing. It never looks like a security decision
    while you are writing it; it looks like a helpful error message. Answer every rejection identically, and put the
    specific message where it is safe to be specific: the sign-up page, or a reset flow that emails the address
    rather than telling the browser.
  - An entity with no `security { }` block is *safe* (deny-all means it grants nothing to anyone) but is usually a
    grant someone forgot to write — the app cannot read its own data.
  - **A credential written to the log.** `Log.*(… someHash, someToken …)` — a log line is not private (it ships to a
    sink, is retained, often indexed), and nothing redacts it for you. Log an id or an email that identifies the record,
    never the credential field itself. (Only fields *derived* as a credential or ending in `Hash`/`Token`/`Secret`/… are
    flagged — an innocently-named `TokenCount` is left alone.)
  - **An app built to be multi-user, with no way to log in.** It declares a `[Principal]`, a `[Role]` enum, and a gated
    surface — a secure-by-default page or a [role-grant](https://osysharp.com/reference/security/role-grants/) table — so it plainly means to have
    users; but not one function is an `[AuthMethod]`, so there is no login path at all. Nobody can authenticate, so
    nobody can pass the deny-all gate those pages and grants sit behind: the app compiles, its data model is complete,
    and not one real user can get in. Add a login `[AuthMethod]` and wire it in `app.AuthBootstrap`
    ([auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/)). A fully public app — no `[Principal]` — is a valid choice and is never flagged; nor
    is a `[Principal]` modelled as plain data with no roles or gated pages yet.
  - **A rule nobody ever tried to break.** An entity whose rules can deny, and no test acting as a principal proves
    they do; or an [invariant](https://osysharp.com/reference/entity/invariants/) no `Assert.Throws` proves bites. A rule you have not tested is a
    rule you only believe you wrote — and an untested invariant does not fail loudly when it rots: the day someone
    deletes it to make an import work, the suite is still green. A `[Test]` that asserts nothing at all is reported
    for the same reason.

    The two refusals are proved differently, and the difference matters. A denied **write** throws, so `Assert.Denied`
    settles it on its own. A denied **read** does not throw — row security is part of the query, so the row was never
    in the result set — and you prove it with an empty result. But an empty result is evidence of a denial *only if
    there was something there to deny*: on its own it passes just as happily when the rule refuses everyone, when the
    table is empty, or when the row was never created. So **pair it** — show that somebody *can* see a row of that
    entity, right beside the principal who cannot. Unpaired, the assertion is reported, because it does not yet prove
    what it appears to.
  - **A rule that cannot say why.** An invariant or a `[Pattern]` with no message refuses the user with the rule
    itself — and a [pattern](https://osysharp.com/reference/entity/constraints/)'s rule is a regular expression, which explains nothing.
  - **An unbounded `string`.** No `[MaxLength]` means the caller decides how much you store. Fine for prose; wrong
    for a code, a name or a status.
  - **An enum value that reads as a run-together word.** Without a `[Label]` label, an enum member is shown by its
    own name — perfect for `Draft`, and wrong the moment there are two words: the grid cell says "InProgress". Only
    multi-word members are reported; a single-word one needs no label.
  - **Work that only shows up on real data.** A query materialized with no `Take` fetches however many rows happen to
    exist; a `Skip` with no `OrderBy` lets page 2 repeat a row from page 1; reading a child collection in a loop over
    parents is one query per parent; and the same query run twice does the work twice — and may give two different
    answers. All four are correct, fast on a laptop, and the reason an app that worked in development falls over in
    production.
  - **A remote call that assumes it works.** An outbound call ([Http.*](https://osysharp.com/reference/http/facade/)) has two failure modes and they need two
    different answers. The *network* throws — a timeout, a DNS failure, a refused connection — and with no `try` that
    kills the function outright, so the caller sees an internal error instead of the failure you meant to handle. The
    *response* does not throw: a 404 or a 500 comes back as an ordinary result, so code that never looks at
    `IsSuccess` carries on and uses the error page's body as though it were the answer — wrong data, no stack trace,
    no log line.
  - **A function that reaches the network through something it calls.** The call may be nowhere in the function's own
    text — a helper makes it — and the function still has no `try`. A timeout down there kills this one exactly as
    dead. Nothing you can read in it warns you, which is the whole reason the linter looks past the source and at what
    the code actually *does*.
  - **A `catch` that says nothing.** A caught exception with no [log](https://osysharp.com/reference/diagnostics/log/) is a failure the app decided to
    survive and then forgot. In production it is invisible: no trace, no count, and no way to answer why the numbers
    are off.

- **CONSIDER** — a candidate for your judgment, reported with its evidence. Never an error.
  - A `[Unique]` or `[Pattern]` field with no `[Required]`: every constraint except `[Required]` lets null through, so
    two rows may both have no value and not collide. If the field is genuinely optional that is exactly right — and if
    you read `[Unique]` as "every row has one", it does not say that. Only you know which was meant, so the linter asks
    rather than asserts.
  - **A routed page with no title.** A page declares its name with `[Title("…")]` (the chrome/route name a breadcrumb
    reads) or a `meta { title = "…"; }` block (the SEO `<title>`). A routed page with neither is a nameless browser tab
    and an accessibility gap — a screen reader announces a page by its title on navigation. A title-less route can be
    deliberate (a redirect-only page), so it is a candidate, not an error.
  - **An editable field under a rule the browser can't pre-empt, in a form that catches nothing.** Plain field rules
    (`[Required]`/`[MaxLength]`/`[Pattern]`/…) are surfaced for you — the input carries them as native attributes, so the
    browser blocks bad input and paints the invalid state without any app code. Two rules can't work that way and still
    throw a `ValidationException` on save: a cross-field `invariant` (`Paid <= Total`), which the client can't evaluate;
    and `[Unique]`, enforced by the atomic DB index — a client "is this taken?" check *races* with the index, so the
    check is a UX nicety and the catch is the real guard. If a form edits such a field and catches no error, the user
    sees a raw failure — catch it where the form saves and show the message. If the component handles errors at all, it
    is not flagged.
  - **A class method that quietly hands off to the server.** A class method is client-runnable code, so a call it makes
    to a function or method that runs on the server is a network round trip — and nothing at the call site shows it;
    whether the callee stays on the client is a fact about *its* body, not the call. Now that almost everything runs on
    the client, a server hop is the notable exception. If it is intended, leave it; if the method was meant to stay
    client-side, keep the server-only work off its path.
  - **A number/date format that runs on the server.** `value.ToString(format[, culture])` formats in the browser only
    for the specifiers the client reproduces byte-identically; anything else fails closed to the server — a round trip,
    invisible at the call site. A Double outside `N`/`F`/`C`/`P`, a custom pattern over a Double (which also *rounds
    differently*), a runtime-built format or culture, an unsupported specifier under a culture, or an unsupported date
    format all round-trip. The finding names which, and the fix (switch the specifier, use a `Decimal`, make the format
    or culture literal). If the round trip is fine, leave it.

Each finding names the rule, what it is about, and how to close it.

`--strict` makes any MUST-tier finding fail the run, so it can gate a ship. `--json` writes the findings as JSON, for
a coding agent or a CI step.

This is a growing rule set, not a finished one — rules are added as patterns emerge. To see what the app *is* rather
than what it is missing, use [Understanding your app](https://osysharp.com/reference/local/understanding-your-app/).

### Which rules exist?   {#lint-rules}

Every rule the linter knows, from the compiler's own catalogue. This list is generated when the page is built, so
it cannot name a rule this release does not have; `osy lint --rules` prints the same list from the binary. A count
on its own would be a vanity number — the names are the point: each one says what it catches.

**A rule id is a search term.** Type the id the linter printed into `osy docs` and it answers: the page that
documents the rule, or — for a rule no page discusses on its own — this section, with the rule's own line above it
so you learn what it catches before anything opens.

⚠ The example below uses a placeholder rather than a real id ON PURPOSE — naming a specific undocumented rule here
would make THIS page the one page that mentions it, which is enough to make the lookup treat this page as the rule's
OWN documentation instead of falling through to this catalogue. That is a real trap (it happened once — a worked
example in this exact spot broke its own guard), and a placeholder cannot fall into it.

```console
$ osy docs <a rule id no page discusses on its own>
matched on lint rule <that id> (<TIER>): <what it catches> — no page documents it on its own, so the linter's
catalogue → local-checking-your-app
```

**129 rules** — 33 MUST, 75 SHOULD, 21 CONSIDER — grouped by what they judge.

**Security** · 32 rules

| Rule | Tier | Catches |
|---|---|---|
| `security-auth-role-tests-nothing` | MUST | an armed auth role that no guard anywhere ever tests |
| `security-authmethod-entity-row-filtered` | MUST | the login's own entity granted only by a row filter the auth principal cannot satisfy |
| `security-credential-mask-hides-it-from-the-login` | MUST | an unconditional deny on the credential also hides it from the login that must read it |
| `security-grant-edit-without-create` | MUST | a caller who may update or delete a role grant they could not have created |
| `security-grant-write-unguarded` | MUST | a role-grant table whose writes are open, so a caller can hand themselves a role |
| `security-hmac-compared-non-constant-time` | MUST | an HMAC tag compared with ==, which leaks how much of a guess was right |
| `security-jwt-issued-unverified` | MUST | a JWT issued before the credential was verified |
| `security-login-enumerates-users` | MUST | a login whose refusal reveals which addresses have an account |
| `security-oauth-signup-no-existing-check` | MUST | an OAuth sign-up that creates a user without checking for an existing one |
| `security-partial-exposes-platform-credential` | MUST | a partial security block on a platform entity that opens a credential column |
| `security-password-compared-directly` | MUST | a stored hash compared with == to a fresh hash, which can never be true |
| `security-password-echoed-in-clear` | MUST | a password field rendered back as visible text |
| `security-principal-credential-client-exposed` | MUST | a principal's credential field that can reach the browser |
| `security-principal-login-field-not-unique` | MUST | the field a login looks users up by is not unique |
| `security-reset-token-never-delivered` | MUST | a reset secret minted but never delivered, so nobody can finish the flow |
| `security-sensitive-field-exposed` | MUST | a readable row carries a password hash, token or secret with no field-level deny |
| `security-signup-cannot-create-the-principal` | MUST | a sign-up that runs as a principal with no create grant on the user entity |
| `security-weak-random` | MUST | a token or secret drawn from a seedable Random |
| `security-anon-page-calls-gated-function` | SHOULD | an anonymous page that calls a function anonymous callers cannot reach |
| `security-anon-page-reads-ungranted-entity` | SHOULD | an anonymous page that reads an entity nobody anonymous may read |
| `security-app-creates-what-its-block-denies` | SHOULD | a function that creates rows its entity's security block denies to every caller |
| `security-authz-without-authn` | SHOULD | a principal, roles and gated pages, but no login function at all |
| `security-callback-url-widens-an-authorize` | SHOULD | a CallbackUrl minted for an event that declares Authorize, which the link then bypasses |
| `security-classified-field-audited-unredacted` | SHOULD | a classified field written to the audit trail unredacted |
| `security-concurrency-check-without-its-trail` | SHOULD | a concurrency check on an entity whose audit trail is off |
| `security-entity-no-block` | SHOULD | an entity with no security block, which grants nobody anything — usually a forgotten grant |
| `security-integration-role-granted-by-signup-order` | SHOULD | a privileged role handed to whoever signs up first, in an app that mints per-user API keys |
| `security-login-reveals-which-credential-failed` | SHOULD | a login that answers a wrong address and a wrong password differently |
| `security-password-typed-in-clear` | SHOULD | a password bound to a plain text field instead of a password field |
| `security-secret-in-log` | SHOULD | a credential field written to the log |
| `security-signup-no-password-policy` | SHOULD | a sign-up that accepts any password at all |
| `security-auth-trail-disabled` | CONSIDER | the authentication audit trail switched off |

**Authentication** · 1 rule

| Rule | Tier | Catches |
|---|---|---|
| `auth-bootstrap-without-app-auth` | CONSIDER | an AuthBootstrap with no app.Auth, so only its own methods can sign in |

**Data model** · 14 rules

| Rule | Tier | Catches |
|---|---|---|
| `data-category-derived-from-free-text` | MUST | a category list built from the rows' own free-text values, which fragments on the first typo |
| `data-root-dialog-cannot-confirm` | MUST | a root dialog that never calls Dialog.Confirm, so nothing it edits can be saved |
| `data-unique-swapped-within-one-commit` | MUST | two rows that swap a unique value inside one commit, which the index refuses |
| `data-write-never-committed` | MUST | a write that is never committed |
| `data-caught-write-fault-left-staged` | SHOULD | a caught write fault whose failed changes are left staged instead of discarded |
| `data-detached-child-query` | SHOULD | children fetched by a standalone query instead of the parent's collection |
| `data-enum-member-no-label` | SHOULD | a multi-word enum member with no Label, shown as one run-together word |
| `data-required-without-a-message` | SHOULD | a Required field with no message for the refusal |
| `data-rule-without-message` | SHOULD | an invariant or pattern with no message, so a refusal shows the rule itself |
| `data-unbounded-string` | SHOULD | a string with no MaxLength, so the caller decides how much you store |
| `data-uniqueness-guarded-only-in-an-action` | SHOULD | uniqueness checked in an action instead of declared on the field |
| `data-constraint-lets-null-through` | CONSIDER | a Unique or Pattern field that is not Required, so null satisfies it |
| `data-row-compared-by-id` | CONSIDER | rows compared by Id by hand where == already is row identity |
| `data-unique-editable-unchecked` | CONSIDER | a Unique field edited in a form that catches no error on save |

**Correctness** · 10 rules

| Rule | Tier | Catches |
|---|---|---|
| `correctness-aggregate-over-pending-writes` | MUST | an aggregate over rows written but not yet committed, so the total is short |
| `correctness-call-has-no-sql-form` | MUST | a call inside a query with no SQL form |
| `correctness-nullable-tested-for-zero` | MUST | a nullable tested for zero, which null passes |
| `correctness-parse-that-answers-zero` | MUST | a parse that answers zero on bad input instead of failing |
| `correctness-egress-that-nothing-calls` | SHOULD | an outbound call declared that nothing in the app ever invokes |
| `correctness-external-value-replaced-by-a-literal` | SHOULD | a missing external value replaced by a literal that looks like real data |
| `correctness-freshness-stamp-with-no-source` | SHOULD | a FetchedAt-style field in an app that has no egress to have fetched anything from |
| `correctness-member-read-off-a-nullable` | SHOULD | a member read off a nullable that may be null |
| `correctness-null-substituted-for-a-value` | SHOULD | a null replaced in arithmetic by a value indistinguishable from a real one |
| `correctness-pool-slot-credited-to-its-assignee` | SHOULD | a pool slot's own arm reading its `Assignee`, which is nothing unless somebody claimed — `actor` is who acted |

**Cost** · 8 rules

| Rule | Tier | Catches |
|---|---|---|
| `cost-child-read-without-include` | SHOULD | a child collection read with no Include on the parent query |
| `cost-index-of-in-its-own-loop` | SHOULD | an IndexOf inside the loop over the same list |
| `cost-n-plus-one` | SHOULD | a child read inside a loop over parents, one query per parent |
| `cost-page-reads-the-whole-table` | SHOULD | a page that reads the whole table |
| `cost-paging-without-an-order` | SHOULD | a Skip with no OrderBy, so page 2 can repeat page 1 |
| `cost-the-same-query-twice` | SHOULD | the same query run twice in one function |
| `cost-unbounded-read` | SHOULD | a query materialized with no Take |
| `cost-clause-calls-the-server-per-element` | CONSIDER | a clause that calls the server once per element |

**Reliability** · 3 rules

| Rule | Tier | Catches |
|---|---|---|
| `reliability-http-result-unchecked` | SHOULD | an HTTP result used without checking IsSuccess |
| `reliability-outbound-call-unguarded` | SHOULD | an outbound call with no try, so a timeout kills the function or re-runs the workflow body |
| `reliability-reaches-the-network-unguarded` | SHOULD | a function or workflow body that reaches the network through a helper, with no try |

**Observability** · 1 rule

| Rule | Tier | Catches |
|---|---|---|
| `observability-catch-without-log` | SHOULD | a catch that logs nothing |

**Workflows** · 8 rules

| Rule | Tier | Catches |
|---|---|---|
| `workflow-dead-end-state` | MUST | a non-terminal state with no way out |
| `workflow-ambient-clock-in-a-durable-body` | SHOULD | the ambient clock read inside a durable body, which replays wrong |
| `workflow-clock-without-route` | SHOULD | a deadline that passes with nothing routed to happen |
| `workflow-initial-state-never-observed` | SHOULD | an Initial state whose Start body always redirects, so no run is ever in it |
| `workflow-no-success-terminal` | SHOULD | a workflow whose every ending is a cancel or an error |
| `workflow-slot-open-to-everyone` | SHOULD | a slot offered to everyone |
| `workflow-unreachable-state` | SHOULD | a state nothing can ever enter |
| `workflow-repeated-completion-condition` | CONSIDER | the same completion condition repeated at the end of several route arms |

**UI** · 37 rules

| Rule | Tier | Catches |
|---|---|---|
| `ui-app-has-no-home-page` | MUST | no page serving the app's front door |
| `ui-toggle-handler-writes-it-again` | MUST | a toggle whose handler writes the value the toggle already wrote |
| `ui-toggle-written-as-a-button` | MUST | a boolean written as a button instead of a toggle |
| `ui-action-never-invoked` | SHOULD | an action nothing in any render can invoke |
| `ui-atom-where-the-kit-has-a-control` | SHOULD | a raw atom hand-built where the kit ships the control |
| `ui-button-indistinguishable-from-a-text-field` | SHOULD | a button styled so it reads as a text field |
| `ui-component-reimplements-a-kit-control` | SHOULD | a component that hand-rolls what a bundled kit control already does |
| `ui-control-call-without-an-accessible-name` | SHOULD | a control call that passes no accessible name |
| `ui-currency-without-a-culture` | SHOULD | a currency formatted with no culture |
| `ui-date-kept-as-a-string` | SHOULD | a date kept as a string and bound to a plain text box |
| `ui-draft-field-ghosts-its-own-list` | SHOULD | a draft row created at mount on a component that lists the same entity, so it appears in its own list |
| `ui-enum-rendered-without-its-label` | SHOULD | an enum rendered by its member name instead of its label |
| `ui-guard-and-action-disagree-about-the-list` | SHOULD | a guard and its action reading two different lists |
| `ui-inert-affordance` | SHOULD | a control that accepts the click and does nothing |
| `ui-input-without-an-accessible-name` | SHOULD | an input with no accessible name |
| `ui-key-read-with-no-key-surface` | SHOULD | Keyboard.Down asked about a key no element declares, so it is false forever |
| `ui-label-drawn-twice` | SHOULD | a label drawn twice for one control |
| `ui-nondeterministic-render-slot` | SHOULD | a render-slot value whose behaviour depends on what else its expression reads |
| `ui-page-root-flush-against-the-viewport` | SHOULD | a page root that paints a surface and sits welded to the viewport edge |
| `ui-page-server-read-with-no-skeleton` | SHOULD | a page that reads from the server with nothing shown while it waits |
| `ui-row-guard-reads-the-unfiltered-list` | SHOULD | a per-row guard that reads the unfiltered list |
| `ui-spacing-step-looks-like-pixels` | SHOULD | a spacing argument that reads as pixels but is steps on the 0.25rem scale |
| `ui-state-nothing-reads` | SHOULD | a state field nothing reads |
| `ui-text-field-bound-to-a-number` | SHOULD | a text field bound to a number |
| `ui-theme-primary-collides-with-a-tone` | SHOULD | a theme's Primary is too close to a semantic tone the app also paints, so a destructive action looks ordinary |
| `ui-theme-token-shadows-nothing` | SHOULD | a theme token named to shadow a kit token that does not exist |
| `ui-control-state-not-announced` | CONSIDER | a control whose state a screen reader is never told |
| `ui-control-without-an-accessible-name` | CONSIDER | a control with no accessible name |
| `ui-data-read-declared-inside-render` | CONSIDER | a data read declared inside a render instead of as a field |
| `ui-date-rendered-without-a-format` | CONSIDER | a date rendered with no format |
| `ui-editable-field-no-error-surface` | CONSIDER | a field under a rule the browser cannot pre-empt, in a form that catches nothing |
| `ui-editable-field-write-policy-unreflected` | CONSIDER | an editable field whose write is gated by a declared policy the input does not reflect |
| `ui-mount-hook-is-a-fetch` | CONSIDER | an on-mount hook that only loads data a field could declare |
| `ui-page-no-title` | CONSIDER | a routed page with neither a Title nor a meta title |
| `ui-rank-rendered-as-a-position` | CONSIDER | a stored rank drawn as the reader's position in a loop over a filtered list, so it reads 1, 3 |
| `ui-raw-style-literal-repeated` | CONSIDER | the same raw style literal repeated where a token belongs |
| `ui-rendered-list-query-not-live` | CONSIDER | a rendered list bound to a query that is not live |

**Formatting** · 3 rules

| Rule | Tier | Catches |
|---|---|---|
| `format-throws-on-double` | MUST | an integer-only format on a Double, which throws at runtime |
| `format-double-custom-rounds-differently` | CONSIDER | a custom pattern on a Double that rounds differently on the client |
| `format-runs-on-the-server` | CONSIDER | a format the browser cannot reproduce, so it round-trips to the server |

**Client** · 1 rule

| Rule | Tier | Catches |
|---|---|---|
| `client-server-hop-in-class-method` | CONSIDER | a class method that quietly makes a server round trip |

**Testing** · 11 rules

| Rule | Tier | Catches |
|---|---|---|
| `testing-login-untested` | MUST | a login no test proves both ways |
| `testing-scope-names-text-not-a-container` | MUST | a test scope that names text rather than a container |
| `testing-app-has-no-tests` | SHOULD | an app that ships no tests at all |
| `testing-denial-provable-by-its-setup` | SHOULD | a denial the setup alone would prove, with nothing there to deny |
| `testing-gated-read-outside-runas` | SHOULD | a gated read asserted outside a runas, so a denial passes as empty |
| `testing-invariant-untested` | SHOULD | an invariant no Assert.Throws proves bites |
| `testing-page-never-driven` | SHOULD | a routed page no test ever visits |
| `testing-security-rule-untested` | SHOULD | a rule that can deny, and no test acting as a principal proves it does |
| `testing-test-without-assertion` | SHOULD | a test that asserts nothing |
| `testing-visible-on-a-number` | SHOULD | an Assert.Visible on a bare number, a contains-check the whole page can satisfy |
| `testing-write-denial-unproven` | SHOULD | a denied write with nothing anywhere proving the same write can succeed for anybody |


### What does it find in the sample apps?   {#lint-sample-run}

`osy lint` run over every sample app in the repository when this page was built — the same call a downloader
makes, on the same source. A rule set is only as credible as what it says about the apps its authors ship, so the
result is published whether or not it is clean.

**36 apps, 302 files: 1 MUST, 599 SHOULD and 117 CONSIDER findings.**

| App | Files | MUST | SHOULD | CONSIDER | Distinct rules |
|---|---|---|---|---|---|
| `agent-expenses` | 18 | 0 | 46 | 9 | 10 |
| `arcade` | 28 | 0 | 51 | 22 | 11 |
| `auth-demo` | 6 | 0 | 0 | 0 | 0 |
| `chart-demo` | 8 | 0 | 5 | 4 | 5 |
| `chat-demo` | 6 | 0 | 10 | 5 | 8 |
| `chat-room` | 6 | 0 | 16 | 2 | 9 |
| `concurrency` | 9 | 0 | 13 | 3 | 9 |
| `dialog-demo` | 6 | 0 | 17 | 1 | 8 |
| `docs-site` | 9 | 0 | 13 | 2 | 10 |
| `dropdown-demo` | 5 | 0 | 9 | 3 | 10 |
| `ember` | 15 | 0 | 38 | 8 | 13 |
| `entity-inheritance` | 9 | 0 | 35 | 0 | 9 |
| `file-manager` | 11 | 0 | 15 | 5 | 12 |
| `generic-grid` | 5 | 0 | 11 | 9 | 10 |
| `gestures` | 2 | 0 | 1 | 2 | 3 |
| `gridprobe` | 3 | 0 | 4 | 1 | 3 |
| `hello-osy` | 4 | 0 | 2 | 1 | 2 |
| `kanban` | 5 | 0 | 18 | 4 | 13 |
| `markdown-demo` | 6 | 0 | 0 | 0 | 0 |
| `media-demo` | 4 | 0 | 4 | 1 | 5 |
| `memory-lab` | 7 | 0 | 13 | 1 | 6 |
| `motion` | 3 | 0 | 1 | 0 | 1 |
| `shell-arrangements` | 7 | 1 | 11 | 1 | 4 |
| `shell-showcase` | 10 | 0 | 15 | 0 | 1 |
| `shop` | 6 | 0 | 12 | 1 | 5 |
| `tabbed_admin_paused` | 18 | 0 | 40 | 20 | 17 |
| `template-stretch` | 5 | 0 | 9 | 6 | 9 |
| `todo` | 3 | 0 | 0 | 0 | 0 |
| `wf-approvals` | 9 | 0 | 24 | 1 | 9 |
| `wf-expense-hitl` | 9 | 0 | 20 | 0 | 7 |
| `wf-fanout-quorum` | 11 | 0 | 27 | 0 | 7 |
| `wf-nightly-digest` | 7 | 0 | 12 | 0 | 5 |
| `wf-order-saga` | 9 | 0 | 31 | 0 | 7 |
| `wf-signup-invite` | 11 | 0 | 20 | 1 | 12 |
| `wf-supplier-dispatch` | 6 | 0 | 21 | 0 | 6 |
| `wf-support-sla` | 16 | 0 | 35 | 4 | 13 |


## Examples       {#examples}

```console
osy lint                  # what this app is missing
osy lint --strict         # fail the run on any MUST-tier finding
osy lint --json           # findings as JSON
osy lint --rules          # every rule: id, tier, what it catches
```

## See also       {#see-also}

[Understanding your app](https://osysharp.com/reference/local/understanding-your-app/) — the resolved model: what the app *is*.

[Explaining your app's security](https://osysharp.com/reference/local/explaining-your-app/) — who can do what, in plain English; `--with-findings` folds these findings into it.

[secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — the deny-all default the security rules are written against.

[security { }](https://osysharp.com/reference/security/entity-security/) — how to declare an entity's access rules.


---

<!-- https://osysharp.com/reference/local/compiling-your-app/ -->

# Compiling your app

> Compiles your app's source into the app on the local platform — the inner-loop compile-and-apply. It applies additive changes directly; non-additive ones (drops, renames, type changes) are gated behind a migration.

<!-- id: local-compiling-your-app · area: local · stability: stable · html: https://osysharp.com/reference/local/compiling-your-app/ -->

## Summary        {#summary}

Compiles the source on your disk into your app on the local platform ([Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/)) — the
inner-loop compile-and-apply. Additive changes (new entities, fields, functions) apply directly; changes that would
remove or reshape existing data are held until you authorise them with a migration.

## Signature      {#signature}

```console
osy compile [path] [--prune] [--generate-migration] [--migration <file>] [--json]
```

## Description    {#description}

`osy compile` finds the local platform for your project, ensures your app exists there, and compiles the current source
into it. It needs no account and no credentials — the local platform is yours. If no platform is running for the
project, it **starts one for you** in the background and leaves it running, so you never have to start a server first —
the compile is a single command. (See [Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/), and [Stopping the local platform](https://osysharp.com/reference/local/stopping-the-local-platform/) to
shut it down.)

- `--prune` reconciles the app to the source, dropping metadata the source no longer declares. On a local (Development)
  app these apply; removing something in production requires a migration.
- `--generate-migration` writes a migration template listing any non-additive changes, each marked `acknowledged =
  false` for you to review — it does not apply anything.
- `--migration <file>` applies a migration you have reviewed and authorised.
- `--json` writes the compile result as JSON.

Compiling against a remote platform is the operator command `osyrin app compile` — the same compile, a different
target.

## Examples       {#examples}

```console
osy compile                       # starts the local platform if needed, then applies the current source
osy compile --generate-migration  # preview a migration for any non-additive changes
```

## See also       {#see-also}

[Importing data](https://osysharp.com/reference/local/importing-data/) — load the app's rows, the other half of "copy this app and it works".

[Running a function](https://osysharp.com/reference/local/running-a-function/) — run one of the app's functions once it is compiled.

[Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/) — the local platform your app compiles into.

[Launching your app](https://osysharp.com/reference/local/launching-your-app/) — open the app in a browser after compiling.

[Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) — run your app's tests (which compile the source too).


---

<!-- https://osysharp.com/reference/local/explaining-your-app/ -->

# Explaining your app's security

> Explains your app's declared authorization in plain English — who can read, create, update and delete each entity, which fields are masked, each page's auth posture, and where a page's access does not line up with the data it shows.

<!-- id: local-explaining-your-app · area: local · stability: stable · html: https://osysharp.com/reference/local/explaining-your-app/ -->

## Summary        {#summary}

Turns your app's declared authorization into a plain-English report — **who can do what**, per entity and per page —
so you can reason about access across the whole app without reading a single security predicate. It needs no platform
and no database: it parses and resolves, nothing else.

## Signature      {#signature}

```console
osy explain [path] [--json] [--with-findings]
```

## Description    {#description}

Your app states access as declarations — a `security { }` block on an entity, a `policy`, an `[Authorize]` on a page.
Each is a precise rule, but reading them one file at a time never adds up to the question you actually have: *across
this whole app, who can read the customer's card number? who can create an order? which page is public?* `osy explain`
answers that. It walks every declaration and writes it out in sentences.

Because access here is **declared, not coded**, the translation is exact and repeatable — the same rules always produce
the same words. It is not a summary that guesses; it is a rendering. When a rule uses a shape the report cannot state
plainly, it says so — it prints the shape faithfully and marks it **`(needs review)`** rather than paraphrasing a
meaning it cannot stand behind. A sentence with no such mark is one you can rely on.

The report has three parts.

**The data surface — per entity.** For every table-backed entity, who may **read**, **create**, **update** and
**delete** it, in one line each:

- A role check reads as *"callers who hold the Staff role."* A grant to signed-in users reads as *"any signed-in
  user."* A grant that also admits anonymous visitors reads as *"anyone, including anonymous visitors (public)."*
- A row filter reads as *"only their own rows (where Owner is the caller)."* Membership through a related table reads
  as *"only rows they belong to (via Membership)."* A membership that also demands a **role** on that record names
  it: *"only rows of an Organization where the caller's Membership has Role = Admin"* — and the same rule written
  through a parameterised policy, `where IsAdmin(Organization)`, reads as that same sentence.
- An entity that grants nothing reads as *"no one (denied by default)"* — the honest reading of a `security { }` block
  with no matching rule, or of an entity with no block at all under [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/).

Every condition a rule carries reaches the sentence. When a membership lambda holds a condition the report cannot
phrase — `&& m.Active == true`, say — the whole rule is printed as written and marked `(needs review)`; it is never
narrowed to the weaker sentence with that condition silently gone. A sentence about *less* restriction than the rule
declares is the one thing this report must not produce, because it is read in place of a review.

It also lists **field masking**: a field a rule withholds (*"the CardNumber field is never returned to any reader"*),
and a `[Classification]` mask (*"Ssn is classified Secret — masked from readers below Secret"*), noting where a field
is also redacted from the audit trail.

**The UI surface — per page.** Access is enforced at the data, but each routed page also declares its **own** posture,
and the two are independent — so the report shows both. Per page: its route; whether it is public (`[AllowAnonymous]`),
gated by a policy (`[Authorize(P)]`, stated as the same English the data surface uses), open to any signed-in user
(the secure-by-default rule), or a composable fragment that inherits its host's gate; the entities it reads and writes;
and any policy-aware controls it carries (a button or field that reflects a policy). A composable that touches no data
is left out — the report is about pages that matter to access, not every presentational primitive.

**The coverage cross-check.** The reason to put the two surfaces side by side is to catch where they **disagree**. A
page's gate opens the screen, but the row read is gated separately at the entity, and the two do not inherit — so a
page that lets in a caller the data will deny renders fine and shows an empty list, with no error to point at. The
report flags every such gap: a public page over data that is not anonymously readable, a signed-in page over data only
a role may read, or a page that reads an entity nothing grants a read to. It only flags what it can prove — a page that
plainly admits **more** than the data serves. It does not guess a direction between two different policies.

**Output.** Markdown by default — a document you can read, review, or commit. `--json` writes the whole report as
JSON, the form to hand to a coding agent or to diff between two revisions; the English sentences ride **inside** the
JSON, so the machine-readable surface is also the readable one. `--with-findings` additionally folds in the maturity
linter's security and UI advice inline (see [Checking your app](https://osysharp.com/reference/local/checking-your-app/)) — off by default, because the report's job is
to state what your rules **are**, and the findings are what a reviewer would then **suggest**. Exit is non-zero when
the source did not fully resolve, so the report is partial and says so.

For the machine model of the whole app — types, relations, function effects — see [Understanding your app](https://osysharp.com/reference/local/understanding-your-app/).
For where the app falls short of a production bar, see [Checking your app](https://osysharp.com/reference/local/checking-your-app/).

## Examples       {#examples}

```console
osy explain                    # the plain-English security report, human-readable
osy explain --json             # the same report as JSON (the English rides in it too)
osy explain --with-findings    # also fold in the linter's security / UI advice
```

The data surface for one entity reads like this:

```text
## `Order` — default-deny

- **Read** — Callers who hold the Staff role; or only their own rows (where Owner is the caller).
- **Create** — Callers who hold the Staff role.
- **Update** — No one (denied by default).
- **Delete** — No one (denied by default).

**Fields**

- The CardNumber field is never returned to any reader.
```

An organisation-scoped grant table — writable only by an admin of the row's organisation, whether the rule is
written inline or through a policy:

```osy title="an org-admin write rule, both spellings" test app=local-explain-org-admin
[Role] enum OrgRole { Admin, Member }

[Principal] entity User {
  [Required, MaxLength(200), Unique] string Email;
  security { allow read when IsAuthenticated; }
}
entity Organization { [Required, MaxLength(80)] string Name;
  security { allow read when IsAuthenticated; } }
entity Membership { [Required] User User; [Required] Organization Organization; [Required] OrgRole Role;
  security { allow read when IsAuthenticated; } }

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

entity RoleGrant {
  [Required] Organization Organization;
  [Required, MaxLength(40)] string Label;
  security {
    allow read when IsAuthenticated;
    allow create, update, delete
      where Membership.Any(m => m.User == user && m.Organization == Organization && m.Role == OrgRole.Admin);
  }
}
entity Invitation {
  [Required] Organization Organization;
  [Required, MaxLength(200)] string Email;
  security {
    allow read when IsAuthenticated;
    allow create, update, delete where IsAdmin(Organization);   // the same rule, named
  }
}
```

reads, for both entities:

```text
- **Read** — Any signed-in user.
- **Create** — Only rows of an Organization where the caller's Membership has Role = Admin.
- **Update** — Only rows of an Organization where the caller's Membership has Role = Admin.
- **Delete** — Only rows of an Organization where the caller's Membership has Role = Admin.
```

A page whose access does not line up with the data it shows is flagged in place:

```text
## `OrdersPage` @ `/orders`

- **Access** — Any signed-in user (secure-by-default; not [AllowAnonymous]).
- **Reads** — Order
  - ⚠ 'OrdersPage' admits any signed-in user, but 'Order's read is gated by a policy/role — a signed-in user outside
    that grant sees an empty 'Order'.
```

## See also       {#see-also}

[Understanding your app](https://osysharp.com/reference/local/understanding-your-app/) — the machine model of the whole app (types, relations, effects).

[Checking your app](https://osysharp.com/reference/local/checking-your-app/) — where the app falls short of a production bar; `--with-findings` folds it in.

[secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — why an entity with no security block grants nothing.

[security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block this report reads.

[principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — the policies it translates into English.


---

<!-- https://osysharp.com/reference/local/giving-a-secret-its-value/ -->

# Giving a secret its value

> Declaring `app.Secrets` names a secret; it does not give it a value, and an app cannot run until something does. `osy secret set` supplies the value on your own machine — it writes the project's gitignored `.secrets` file, which every compile re-applies, and applies it straight away to a running local app. `osy secret list` shows which of your declared secrets still have no value here.

<!-- id: local-giving-a-secret-its-value · area: local · stability: stable · html: https://osysharp.com/reference/local/giving-a-secret-its-value/ -->

## Summary        {#summary}

[declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) declares that your app uses a secret. It does not supply the value — a secret's value never appears
in source. `osy secret set` is how you supply it on **your own machine**, and `osy secret list` shows which declared
secrets still have none.

```console
osy secret set Anthropic          # prompts for the value, so it stays out of your shell history
osy secret list                   # which secrets are declared, and which still have no value here
```

## Signature      {#signature}

```console
osy secret list [--path <dir>] [--json]
osy secret set <NAME> [VALUE] [--path <dir>] [--devname <name>]
```

`NAME` must be a name your app declares in `app.Secrets`. A name it does not declare is refused — with the closest
declared name, when there is one — because a value your app cannot read is a credential stored for nothing.

## Description    {#description}

### Where the value is kept   {#secrets-file}

`osy secret set` writes the value to a file called `.secrets`, beside your `app.osy`. The format is one
`NAME=value` per line, so you can also edit it by hand — the two are exactly equivalent.

```text
Anthropic=sk-ant-...
```

That file is the **durable** source on your machine. Your app's secret values live in the app's database, and resetting
the local platform wipes them; every compile re-applies `.secrets`, so a reset cannot lose the values you set. This is
also why setting a value does not require a running platform: if one is running, the value is applied immediately and
takes effect without recompiling; if not, the next compile applies it.

`.secrets` holds credentials in plain text, so `osy secret set` makes sure it is ignored by git before writing to it —
adding it to your project's `.gitignore` if nothing already covers it. If your project is not in a repository at all, it
says so, because then there is nothing keeping the file out of an archive you share.

### Which secrets still need a value   {#listing}

```console
osy secret list
```

lists every secret your app declares and whether it has a value here. It reads the declarations from your **source**, so
it answers in a project you have never compiled, with no platform running. If `.secrets` sets a name your app does not
declare, `list` reports it: a compile refuses while that is true, and applies nothing.

### Values that are not yours to set   {#deployed}

`osy secret` is the LOCAL surface — your machine, your project. A deployed app's secrets are supplied by whoever
operates the platform it runs on, out of band, and are never read from a file in your project.

A **user-scoped** secret (`new Secret("Name") { UserScoped = true }`, see [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/)) belongs to each user of
your app rather than to the app, so each user supplies their own through the app — there is no single value to set here.

## Examples       {#examples}

Give a declared API key its value and see the result:

```console
$ osy secret set Anthropic sk-ant-...
✓ Secret 'Anthropic' written to .secrets.
  This file is the durable local source: `osy compile` re-applies it, so a `dev --reset` cannot lose it.
  Added `.secrets` to .gitignore — it holds credentials in plain text.
  Applied to the running local app.

$ osy secret list
╭───────────┬─────────╮
│ Name      │ Value   │
├───────────┼─────────┤
│ Anthropic │ ●  set  │
╰───────────┴─────────╯
```

Omit the value to be prompted for it, so it never reaches your shell history:

```console
$ osy secret set Anthropic
Value for Anthropic: ********
```

A name your app does not declare is refused, with the near miss:

```console
$ osy secret set Anthropi sk-ant-...
✗ 'Anthropi' is not declared in `app.Secrets` — did you mean 'Anthropic'?
  This app declares: Anthropic
```

## See also       {#see-also}

[declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — declaring the secrets your app uses, and referencing one by its `Secret.Name` handle.

[Compiling your app](https://osysharp.com/reference/local/compiling-your-app/) — the compile that re-applies `.secrets` to your local app.

[default LLM model (app.DefaultModel)](https://osysharp.com/reference/agent/default-model/) — `app.DefaultModel`, whose `ApiKey` is a declared secret.


---

<!-- https://osysharp.com/reference/local/importing-data/ -->

# Importing data

> Loads your app's own data — the JSON files that live beside its source — into the app on the local platform. Rows are matched by key and updated, so running it twice converges instead of doubling.

<!-- id: local-importing-data · area: local · stability: stable · html: https://osysharp.com/reference/local/importing-data/ -->

## Summary        {#summary}

Loads the rows your app ships with — a catalogue, a country list, tax rates, whatever it needs to be useful on first
open — from JSON files beside its source into the app on the local platform.

`osy compile` ships your app's **code**; this ships its **rows**. Together they make "copy this app and it works" true
of both halves, instead of opening on an empty page.

## Signature      {#signature}

```console
osy import [path] [--file <path>] [--replace] [--as <login>] [--password <pw>] [--json]
```

## Description    {#description}

### Where the files come from   {#files}

Your manifest declares them, the same way it declares source:

```text
data "data/**/*.json";
```

Files load in path order, and that order matters: a row that references another resolves against rows already
imported, so `data/suppliers.json` loads before `data/products.json`. Naming your files is how you control it.
`--file <path>` imports just one file (and repeats), ignoring the glob.

### Running it twice is the point   {#idempotent}

Every file names the property that identifies a row, and import **upserts** by it. Running the same import again
updates what changed and leaves the rest alone — which is what makes it usable in a build loop and in CI, rather than
a one-shot that doubles your catalogue.

**`key` also takes a LIST, which is how you seed a row that no single property identifies.** A link between two
things — a membership, a dependency, an assignment — is identified by both ends together and by neither alone, so
one property cannot key it:

```json title="an edge, keyed by both ends, re-runnable like any other file"
{
  "entity": "Dependency",
  "key": ["Dependent", "Prerequisite"],

  "lookups": { "Dependent": "Task[Code]", "Prerequisite": "Task[Code]" },

  "rows": [
    { "Dependent": "SHIP-2", "Prerequisite": "SHIP-1", "Kind": "Blocks" }
  ]
}
```

The rule is the same one as for a single key — the combination has to identify the row — so it upserts and re-runs
clean. Without this, a join entity has to grow a synthetic identifier that exists only to satisfy the importer,
which is a schema bent around a tool.

`--replace` deletes every existing row of each imported entity first. It destroys rows your files do not mention, so
it is never the default.

⚠ **An upsert needs `update` granted, and the first run will not tell you.** An entity whose `security {}` grants
`create` but not `update` imports perfectly the first time and fails the SECOND, when the rows already exist:

```console
✗ data/03-policies.json row 1: Update of 'ApprovalPolicy' denied — it declares a `security { }` block, and that
  block grants no `update`. An entity with a block denies every operation the block does not name.
```

The fix is a rule in the model, never a flag here — reference data an operator re-seeds is data an operator may
edit, so say so: `allow update when IsOps`. ⚑ The same shape bites in the other direction: an entity that grants
`create` but not **`read`** cannot be MATCHED, so every run creates and the second one doubles the table. Both come
from the same place — an upsert is a read, then a create or an update, and a block that names only `create` grants
one third of it.

### Files a row points at   {#assets}

A row can reference an image or a document by a path relative to **its own data file**. Those bytes travel with the
import, so an imported product photo is served from the app afterwards rather than 404ing.

### Who it imports as   {#principal}

An import is an ordinary data write, so your app's `security { }` decides **every row, on every run**. `--as` chooses
*which* principal the rows are written as; it never chooses *whether* the rules apply. There is no privileged import.

With no `--as`, the import acts as **anonymous** inside your app. Being the app's developer authorises you to *call*
the import — it is not an identity inside the app, so it grants nothing to the rows themselves. An entity that admits
anonymous writes is seeded; one that does not is refused, row by row, with the reason.

```console
$ osy import
✗ data/notes.json row 1: Create of 'Note' denied — it declares a `security` block
```

That is the common case for a real app, and `--as <login>` is the answer: it resolves one of your app's **own** users
and writes as them, so the rows land exactly where that user may write. `<login>` is whatever `app.Auth` binds as its
login field. Add the user first — [Adding an account](https://osysharp.com/reference/local/adding-an-account/) writes a real account through the app's own declared
fields, with the roles the import will need.

**`--as` is a login, not a label**: it takes that user's own password, because naming a principal must never be
enough to become one. Omit `--password` and you are prompted; there is no waiver, in dev or anywhere else.

```console
$ osy user add ops@example.com --password 's3cret' --role Admin
$ osy import --as ops@example.com --password 's3cret'
✓ data/notes.json: 3 created   (ran as ops@example.com)
```

Every run reports which principal it used, so an import that seeded as an admin can never be mistaken for one that
proved an ordinary user could do it. That second reading is the other reason to use `--as`: pointing it at a customer's
own login checks that an import they will run is one they are allowed to run.

### Against a deployed app   {#remote}

`osyrin app import` is the same command against a platform you are logged in to.

## Examples       {#examples}

```console
osy import                                  # everything the manifest's data glob matches, as anonymous
osy import --file data/products.json        # just this file
osy import --as ops@example.com --password 's3cret'   # as one of the app's own users — needed for anything it gates
osy import --replace --json                 # start from scratch, machine-readable result
```

## See also       {#see-also}

[Running a function](https://osysharp.com/reference/local/running-a-function/) — run one of the app's functions, the other half of putting it in a known state.

[Compiling your app](https://osysharp.com/reference/local/compiling-your-app/) — ship the code these rows belong to.

[The inner loop](https://osysharp.com/reference/local/the-inner-loop/) — where this sits in the build-run-look loop.


---

<!-- https://osysharp.com/reference/local/installing-the-editor-extension/ -->

# Installing the editor extension

> Installs the Osy# editor extension that ships with the CLI into VS Code (or a compatible editor). The extension launches the toolchain's own language server (`osy lsp`) and drives the same CLI, so once the CLI is on your PATH it just works.

<!-- id: local-installing-the-editor-extension · area: local · stability: stable · html: https://osysharp.com/reference/local/installing-the-editor-extension/ -->

## Summary        {#summary}

Installs the Osy# editor extension — a small package shipped with the CLI, whose language server is the CLI itself (`osy lsp`) — into VS Code or a compatible editor (Cursor, VSCodium,
Windsurf). The extension brings language intelligence (completions, diagnostics, go-to-definition, the test explorer)
and runs your project through this same CLI.

## Signature      {#signature}

```console
osy vscode install [--editor <cmd>] [--vsix <path>]
```

## Description    {#description}

It finds the extension shipped alongside the CLI, detects your editor's command-line launcher on your PATH (`code`,
`code-insiders`, `cursor`, `codium`, or `windsurf`), and installs it. Reload the editor afterwards to activate it.

The extension drives the CLI through its `osy.cli.path` setting, which defaults to `osyrin` — so as long as the CLI is
on your PATH, no editor configuration is needed.

- `--editor <cmd>` installs into a specific editor (by its CLI command) instead of the first one found.
- `--vsix <path>` installs a specific extension package instead of the one shipped with the CLI.

If no editor CLI is found, it says so — in VS Code you may need to run *"Shell Command: Install 'code' command in
PATH"* first.

### It marks where your code changes side   {#side-crossing-hints}

The extension ghosts a `→ server` (or `→ client`) in front of any call that **leaves the side the body it is written
in runs on**. Nothing in the source says so otherwise: the seam is deliberately invisible, so that you write ordinary
code across it — which is right for correctness and hides the one thing you may need to act on. Each of those arrows
is a network round trip.

What you see on screen — the arrows are the editor's, not something you type:

```text
action Save() {
  → server SaveDraft(draft);      // a round trip
  → server Notify(author);        // …and another. Could these have been one call?
  closed = true;                  // client-side: no arrow, no cost
}
```

**Only the crossings are marked.** A server function that calls three more server functions crosses nothing, and gets
nothing — the arrows appear where the cost is, not on every call to a server function. That is what keeps the
annotation readable in the half of an app that never leaves one side.

The arrow says the direction and nothing about saving. Calling a server function from a page carries your
uncommitted edits along with the call — the server reads your own changes — but it does **not** commit; only an
explicit `UnitOfWork.Commit()` does. See [[realtime-topic#one-unit-of-work]].

## Examples       {#examples}

```console
osy vscode install                 # install into the first editor found on PATH
osy vscode install --editor cursor # install into Cursor specifically
```

## See also       {#see-also}

[Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/) — the local platform the editor's commands run against.

[Launching your app](https://osysharp.com/reference/local/launching-your-app/) — open your app in a browser from the editor or the CLI.

[Launching the page you're on](https://osysharp.com/reference/local/launching-the-page-youre-on/) — what F5 does once the extension is installed.


---

<!-- https://osysharp.com/reference/local/launching-the-page-youre-on/ -->

# Launching the page you're on

> Press F5 in a page file and your app opens at that page, not at the home route, stopping at your breakpoints. Ctrl+F5 does the same without the debugger. The page is the one your cursor is inside.

<!-- id: local-launching-the-page-youre-on · area: local · stability: stable · html: https://osysharp.com/reference/local/launching-the-page-youre-on/ -->

## Summary        {#summary}

Press **F5** while editing a page and your app opens **at that page**, stopping at any breakpoint you have set. The
platform starts if it isn't running, your current source is compiled into the app, and the browser lands on the page
whose component your cursor is inside — so the loop from "editing this screen" to "looking at this screen" is one key.
**Ctrl+F5** is the same launch without the debugger.

## Signature      {#signature}

```console
F5                              # start debugging: breakpoints armed, then the page opens
Ctrl+F5                         # start without debugging: just open the page
Osy#: Launch Page at Cursor     # the Ctrl+F5 gesture from the command palette
```

## Description    {#description}

Neither key needs setup: with the editor extension installed ([Installing the editor extension](https://osysharp.com/reference/local/installing-the-editor-extension/)) they work in
a fresh project with no launch configuration at all. Both run the same launch as [Launching your app](https://osysharp.com/reference/local/launching-your-app/) —
starting the local platform if needed, compiling what is on disk — and then open the route of the page you are
looking at.

**F5 arms your breakpoints first.** It waits for them to register before opening the page, so the page opens already
stopped at one rather than racing past it. **Ctrl+F5** skips all of that and just opens the page.

**The page is the one your cursor is inside, not the first in the file.** A file often declares several pages, and a
cursor is the only thing that says which one you mean.

**Not in a page → the home route, and nothing is asked.** Editing an entity, a function, a test, or a presentational
component is the ordinary case, and F5 there simply opens the app at `/`.

**A page whose route has `{…}` in it can't be opened as written**, so F5 says which parts it needs — `/lists/{id}`
needs `id` — and offers to open the home route instead, or to take the values from you. Cancelling cancels; it does
not quietly open something else.

The cursor is read the moment you press the key, so clicking into another file while the compile runs does not change
where you land.

## Examples       {#examples}

```console
# cursor inside a component declaring [Page("/archived")]
F5       →  breakpoints armed, then http://your-app.localhost:8168/archived
Ctrl+F5  →  http://your-app.localhost:8168/archived

# cursor inside a component declaring [Page("/lists/{id}")] — either key asks
"/lists/{id} needs `id` — it can't be opened as written"
   → Enter values…  ·  Open the root route

# cursor in model/entities.osy — neither key asks anything
http://your-app.localhost:8168/
```

## See also       {#see-also}

[Launching your app](https://osysharp.com/reference/local/launching-your-app/) — the same launch from the command line, and its `--print-url` form.

[Installing the editor extension](https://osysharp.com/reference/local/installing-the-editor-extension/) — installing the extension F5 comes from.

[Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/) — the local platform your app is served by.


---

<!-- https://osysharp.com/reference/local/launching-your-app/ -->

# Launching your app

> Opens your app in a browser, running on the local platform. It compiles the current source first, so what opens reflects what is on disk, then opens the app's local URL.

<!-- id: local-launching-your-app · area: local · stability: stable · html: https://osysharp.com/reference/local/launching-your-app/ -->

## Summary        {#summary}

Opens your app in a browser, served by the local platform ([Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/)). It compiles the source
on your disk into the app first — so what you see is what you just wrote — and then opens the app's local address. Your
app serves its own login and access rules exactly as in production; this only opens the page.

## Signature      {#signature}

```console
osy launch [path] [--print-url] [--no-compile]
```

## Description    {#description}

`osy launch` finds the local platform for your project, ensures your app exists there, compiles the current source into
it, and opens `http://<your-app>.localhost:<port>/` in your browser. Edit, `osy launch`, see the result. If no platform
is running for the project, it **starts one for you** in the background first, so a single command takes you from source
to an open page ([Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/); [Stopping the local platform](https://osysharp.com/reference/local/stopping-the-local-platform/) shuts it down).

- `--print-url` prints the app's URL and opens nothing — for an editor that wants to open the page its own way. In this
  mode the URL is the only thing written to standard output.
- `--no-compile` opens the app as it was last compiled, without recompiling first — a quick re-open.

A brand-new app has no page yet, so its address returns "not found" until you add one — the app is there; it just has
nothing to show at `/`.

If the browser runtime your pages are drawn with is not being served, `launch` says so **before** it opens anything,
and names the command that builds it. Pages are drawn in the browser, so without it the address answers perfectly well
and shows a blank screen — the one failure that looks exactly like a bug in the app you just wrote.

## Examples       {#examples}

```console
osy launch                 # starts the platform if needed, compiles, opens in the browser
osy launch --print-url     # just print http://<app>.localhost:<port>/
```

## See also       {#see-also}

[Launching the page you're on](https://osysharp.com/reference/local/launching-the-page-youre-on/) — press F5 in your editor and land on the page you are editing, not on `/`.

[Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/) — the local platform your app runs on, and how to start it.

[Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) — run your app's tests against the same local platform.


---

<!-- https://osysharp.com/reference/local/listing-local-instances/ -->

# Listing your local platforms

> Lists every local platform on this machine and says which are still running. The build-and-test loop starts one per project on demand, so this is how you find the ones you left up — and which project each belongs to.

<!-- id: local-listing-local-instances · area: local · stability: stable · html: https://osysharp.com/reference/local/listing-local-instances/ -->

## Summary        {#summary}

Shows every local platform this machine has, and which of them are running right now. Each project gets its own — the
loop commands start one on demand ([Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/)) — and [Stopping the local platform](https://osysharp.com/reference/local/stopping-the-local-platform/) stops
exactly one, the project you are standing in. This is the other half: seeing what is up before deciding what to stop.

## Signature      {#signature}

```console
osy instances [--running] [--all] [--stale] [--json]
```

## Description    {#description}

Running platforms are listed first, each with its URL, the process behind it, and how long it has been up. A platform
counts as **running** only when the process it recorded is verified to still be that process — a recorded process id on
its own is not proof, because the system reuses ids, so anything unproven is reported as such rather than guessed at.

- `--running` lists only the platforms with a verified live server.
- `--all` also lists directories that have never run a server. Working in a throwaway project leaves one behind, so
  these accumulate; they are counted in the summary line by default and printed in full with this flag.
- `--stale` lists only the platforms whose project directory no longer exists.
- `--json` emits the list for tools and agents.

The summary line reports the total disk all of them occupy. Each carries a database of its own, so the figure grows
faster than the count suggests — [Reclaiming the disk your local platforms use](https://osysharp.com/reference/local/reclaiming-local-disk/) deletes the ones you have finished with.

Listing never signals anything — it only reads. To stop one, go to its project and run `osy stop`, or name it directly
with `osy stop --devname <name>`. Stopping a platform by killing processes by name is never the right move: another
project's platform, or a test run, can be caught in the same sweep.

## Examples       {#examples}

```console
osy instances --running     # just what is still up
```

```console
osy instances --json        # the same list, for a tool to read
```

## See also       {#see-also}

[Reclaiming the disk your local platforms use](https://osysharp.com/reference/local/reclaiming-local-disk/) — delete the ones you are no longer using.

[Stopping the local platform](https://osysharp.com/reference/local/stopping-the-local-platform/) — stop one of the platforms this lists.

[Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/) — what a local platform is, and how the loop starts one per project.

[Launching your app](https://osysharp.com/reference/local/launching-your-app/) — compile and open your app, starting a platform if there isn't one.


---

<!-- https://osysharp.com/reference/local/reading-a-capability/ -->

# Reading a capability's source

> Prints the source of a capability you opted into with `using`. A capability's source ships inside the platform rather than in your project, so it cannot be grepped or opened — this is how you read what a `using` actually brings into your app, including the reasoning in its comments.

<!-- id: local-reading-a-capability · area: local · stability: stable · html: https://osysharp.com/reference/local/reading-a-capability/ -->

## Summary        {#summary}

Prints the source of a capability. `using Osysharp.Memory;` puts real entities — real tables, with their own access
rules — into your app, but that source lives inside the platform rather than in your project, so no amount of
searching your own files will find it. `osy source` is how you read it. Offline: no server, no account, no database.

## Signature      {#signature}

```console
osy source [name] [--json]
```

`name` is a capability (`Osysharp.Memory`, or just `memory`) **or a type it declares** (`MemoryChunk`). Omit it to list
every capability with what it declares.

## Description    {#description}

A capability is opted into per app in `app.osy` (`use Osysharp.Memory;`) and per file (`using Osysharp.Memory;`). What you
get in return is a set of declared types. Those types are not abstract: they become tables in your app's own database,
carrying the access rules the capability declares.

Because the source is embedded in the platform, three ordinary ways of answering "what did I just agree to" do not
work — you cannot open the file, you cannot grep for it, and it is not in your version control. That gap is what this
command closes.

**It only ever reads.** A capability declares types; the behaviour behind them is the platform's own. So there is no
useful copy to take: a local fork would be a declaration with nothing implementing it, which would compile, shadow the
real one, and then be wrong. The one supported change to a capability entity is its security, and that is written as a
`partial entity` block in your own source:

```osy title="stating security for an entity a `using` brought in" test app=local-reading-a-capability
app Invoicing { use Osysharp.Storage; }

// `use` in the manifest takes the DEPENDENCY; `using` brings its types into THIS file's scope — the same split C#
// makes between a package reference and a using directive. A `partial entity` resolves its target through the
// file's `using`s, so both lines are needed.
using Osysharp.Storage;

[Role] enum AppRole { Staff }
[Principal] entity User { string Email; }

// `FileAsset` is declared by `using Osysharp.Storage;` — this app never declares it, and cannot.
// A partial adds no fields and changes no shape; it states who may reach the type.
partial entity FileAsset {
  security { allow read when IsAuthenticated; }
}
```

For an entity the capability leaves ungoverned — as `FileAsset` is above — what you write is the whole rule.

Some capability tables arrive already governed, because their rows belong to one signed-in user and only the
capability knows that: a chat conversation is yours, not the app's. There your partial **adds** to the rule already
there rather than replacing it, so you can grant a support role extra reach without being able to take the owner's
access away. See [capability rows that belong to a user](https://osysharp.com/reference/security/capability-row-ownership/).

> The UI kit is the exception, and deliberately so. A kit control is presentational — the source you read *is* the
> implementation — so it is meant to be forked. `osy kit` browses it and `osy get ui/<control>` vendors a copy into
> your project to change. See [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/).

## Examples       {#examples}

Every capability, and what each declares:

```console
osy source
```

```text
Platform capabilities — opt in per file with `using <name>;` (and in app.osy)

  Osysharp.Llm.Observability
    LlmAuditEntry, LlmCallLog, LlmStopReason, LlmToolCallLog
  Osysharp.Memory
    ContextType, EntityContext, FileChunk, Reference, ReferenceKind, …
  Osysharp.Storage
    FileAsset, FileGrant, FileGrantLevel, Folder, UploadedFile

The core baseline — in every app; no `using` opts in, and none can opt out

  Osyrin (13 files)
    ActionState, Align, AuditKind, ConnState, Connection, Continuation, … (+32)
```

The **core baseline** is the last section, and it is not one of the choices above it: those tables — the workflow
runtime, the markdown store, the OAuth grant store — are in your app whether you ask for them or not. Read it with
`osy source core`.

One capability's whole source, comments and all:

```console
osy source Osysharp.Memory
```

Or start from a type you met in `osy model` output or in an error, and let it find the `using`:

```console
osy source LlmCallLog
```

```text
# LlmCallLog is declared by `using Osysharp.Llm.Observability;`
# Capabilities/Osysharp.Llm.Observability.osy
```

When the question is only *what fields does it have*, `osy docs` takes the same names and answers shorter — the
member list and the `using`, without the declaration around it:

```console
osy docs FileAsset
```

```text
FileAsset — a platform entity, in scope with `using Osysharp.Storage;`
A stored file asset with hybrid inline/external storage.

  string?      AltText
  string?      MimeType
  string       Name
  FileVersion  CurrentVersion
  bool         IsPublic
  Folder       Folder

the full declaration — doc comments, attributes, security: osy source FileAsset
```

An enum answers with every member it has, which is usually the whole question — `osy docs Size` prints
`Sm · Md · Lg`. `osy docs --list <word>` searches these names too, so a half-remembered type is findable without
knowing which capability declares it.

## See also       {#see-also}

- [Explaining your app's security](https://osysharp.com/reference/local/explaining-your-app/) — which of these your app has actually opted into, and who can read each one,
  including the core baseline every app carries
- [Understanding your app](https://osysharp.com/reference/local/understanding-your-app/) — the resolved model, with capability types bound exactly as the server binds them
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — the kit, which is read the same way but *is* meant to be forked


---

<!-- https://osysharp.com/reference/local/reading-your-data/ -->

# Reading your data

> Evaluates one Osy# expression against your app and prints what it answered — the rows, the count, the projection. The read half of `osy run`, which only ever writes. It is not a query language: the expression goes through the same compiler your source does, so anything the language expresses is evaluable here.

<!-- id: local-reading-your-data · area: local · stability: stable · html: https://osysharp.com/reference/local/reading-your-data/ -->

## Summary        {#summary}
`osy run` puts data in. `osy query` gets it out.

```console
$ osy query "Job.OrderBy(j => j.Position)"
[
  { "Title": "re-plaster the hall", "Room": "hall", "Position": 10 },
  { "Title": "paint the shed",      "Room": "garden", "Position": 20 }
]

$ osy query "Job.Where(j => !j.Done).Count()"
2
```

## Signature      {#signature}
```console
osy query "<expression>"            // evaluate as ANONYMOUS — what a signed-out visitor sees
osy query "<expression>" --as ada@example.com   // …as one of your users — what THEY see
osy query "<expression>" --json     // the whole result envelope, for a script
```

## Description    {#description}

### It is not a query language   {#not-a-query-language}
The expression is lexed, parsed, resolved and run by **the same compiler your source goes through**. There is no
second grammar to learn and none to drift: whatever you could write inside a function body, you can write here.

That includes the refusals. A verb the language does not have comes back in the resolver's own words, with the
closed set it does have:

```console
$ osy query "Job.Nope(j => j.Title)"
✗ unknown query method 'Nope' — on 'Job' rows read from the data store the verbs are: All, Any, Average,
  Count, Distinct, … OrderBy, OrderByDescending, … Where
```

### What it answers with   {#shapes}
Whatever the expression evaluates to, rendered as JSON — because an expression has no single shape:

| you write | you get |
|---|---|
| `Job.OrderBy(j => j.Position)` | an array of rows |
| `Job.Count()` | a number — a scalar is a scalar, not a one-element array |
| `Job.Select(j => j.Title)` | an array of strings |
| `Job.First()` | one row |

A row shows the values the read actually brought back. A column your acting principal may not see is simply absent,
which is the honest rendering of what happened rather than a hole where a value would be.

### Reading as one of your users   {#as}
⭐ **`--as` takes no password, and that is the point.** Anonymous is the wrong default to be stuck with: on an app
with real security an anonymous read returns **nothing**, so the verb would be useless exactly where it matters —
and the only alternative would be reading with security off and returning **everything**. Neither answers the
question you actually have, which is *what does this user see?*

```console
$ osy query "Job.Count()"                       // as a signed-out visitor
$ osy query "Job.Count()" --as ada@example.com  // as Ada
$ osy query "Job.Count()" --as sam@example.com  // as Sam
```

Your `security { }` applies in every case. If the three answers differ, that IS your rules working — this is the
cheapest way to see them do it.

### Why that is safe, and where the line is   {#why-no-password}
⛔ **`osy query` refuses to WRITE.** An expression that creates, updates or deletes is rejected before it runs, and
the check is fail-closed: anything that cannot be proven read-only counts as writing.

That refusal is what makes the password-free `--as` sound. You already have developer authority over this app — you
can compile arbitrary code into it — so *reading* as one of its users grants you nothing you did not already have.
**Writing as them is impersonation**, which authority over an app does not confer over its people, so it stays
behind that user's own password:

```console
$ osy query "new Job { Title = \"sneaky\" }"
✗ `query` READS — this expression writes data, so it is refused here. Anything that creates, updates or
  deletes belongs in one of the app's own functions: run it with `osy run <Function>`.

$ osy run AddJob --arg title="re-plaster the hall" --as ada@example.com --password …
```

### Checking what you just wrote   {#after-a-run}
The pair is the point. `osy run` makes something happen; `osy query` says whether it did:

```console
$ osy run AddJob --arg title="re-plaster the hall"
$ osy query "Job.Count()"
1
```

## See also       {#see-also}
- [Running a function](https://osysharp.com/reference/local/running-a-function/) — the other half: making the app DO something
- [Importing data](https://osysharp.com/reference/local/importing-data/) — loading many rows from a file
- [Querying data](https://osysharp.com/reference/query/index/) — the query surface itself, and what becomes SQL
- [The security model](https://osysharp.com/reference/security/index/) — the rules `--as` is showing you


---

<!-- https://osysharp.com/reference/local/reclaiming-local-disk/ -->

# Reclaiming the disk your local platforms use

> Each local platform carries its own database, so they add up fast. This deletes the ones you are no longer using — showing you what it would remove before it removes anything.

<!-- id: local-reclaiming-local-disk · area: local · stability: stable · html: https://osysharp.com/reference/local/reclaiming-local-disk/ -->

## Summary        {#summary}

A local platform is created per project, on demand, and each one carries a full database of its own — a few hundred
megabytes. Over a few weeks of ordinary work that is tens of gigabytes of projects you have long since moved on from.
This deletes the ones that are no longer in use, and tells you first.

## Signature      {#signature}

```console
osy instances --prune [--yes] [--idle-hours <n>] [--json]
```

## Description    {#description}

**It is a dry run unless you pass `--yes`.** On its own it prints what it would delete, what that would reclaim, and —
just as usefully — everything it is KEEPING, with the reason for each. Read it, then re-run with `--yes`.

An instance is deleted when **both** are true:

- **nothing is running on it.** A platform whose server is still up is never touched, and neither is one whose state
  cannot be established — if it cannot be shown that nothing is running, it stays.
- **it has not been used recently.** A day, by default; `--idle-hours` changes the window.

Note what is *not* a condition: whether the project still exists. A checkout can live for months while you start
platforms in it several times a day, so keeping every instance of every live project would keep almost all of them. A
local platform is disposable by design — rebuilt from your source whenever a command needs one — so deleting one costs
a rebuild and nothing else. Nothing you would miss is stored there; your app's real data lives on the platform you
deploy to.

Everyday use needs none of this: starting a local platform reaps unused ones as it goes. The command is for when you
want to see the number, or clear space now.

[Listing your local platforms](https://osysharp.com/reference/local/listing-local-instances/) reports the total on disk every time you run it, so the figure is visible long before
it is a problem.

## Examples       {#examples}

```console
osy instances --prune              # what would go, and what stays — deletes nothing
```

```console
osy instances --prune --yes        # actually delete them
```

```console
osy instances --prune --idle-hours 4 --yes    # clearing space now
```

## See also       {#see-also}

[Listing your local platforms](https://osysharp.com/reference/local/listing-local-instances/) — what is on this machine, which are running, and how much disk it all takes.

[Stopping the local platform](https://osysharp.com/reference/local/stopping-the-local-platform/) — stop one that is running, so it becomes a candidate.

[The inner loop](https://osysharp.com/reference/local/the-inner-loop/) — where local platforms come from in the first place.


---

<!-- https://osysharp.com/reference/local/recording-a-run/ -->

# Recording a run

> Turns on step recording, so a trace carries the steps that LED to a failure and not only the failure itself. Faults are captured either way — you only need this when the question is "how did it get here?". `--full` also captures the values in scope at every step, for when the question is "why is this value wrong?".

<!-- id: local-recording-a-run · area: local · stability: stable · html: https://osysharp.com/reference/local/recording-a-run/ -->

## Summary        {#summary}

Records every step of every run, so that [Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/) can show you the path the code took. It is off
by default, because recording every step is the expensive half of tracing — and it is the half you usually do not need.

## Signature      {#signature}

```console
osy trace start
osy trace start --full
osy trace stop
```

## Description    {#description}

**Read this before turning it on:** a **fault is always captured**, recording or not. If a run throws, the place it
threw and the values that were in scope there are retained regardless — that capture costs nothing until something
actually throws. So if your question is *"why did this fail?"*, you do not need `osy trace` at all. Just run
[Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/).

Turn recording on when your question is different: **"how did it get there?"** — which branch it took, whether a loop
ran at all, what order things happened in. That is the story recording adds, and it is why the switch exists rather
than being always on: a step-by-step record of every run on the server is real overhead, so you turn it on for the run
you care about and off again afterwards.

What plain `osy trace start` records is the **path**, not the state at every step — where execution was, statement by
statement. The full state is captured where it earns its cost: at the fault. This is what keeps recording affordable
enough to leave on while you reproduce something.

**When the path is not enough, `--full` adds the state.** A wrong *result* — not a crash — is the case the path alone
cannot answer: you can see which branch ran, but not what the values were. `osy trace start --full` captures the locals
in scope at **every** step, on both sides of a client↔server run, so [Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/) shows each variable
as it changes. It is the heaviest mode — a snapshot of the stack at every step of every run — so turn it on for the run
you are chasing and off again after. A record is shown exactly as everywhere else: as its type and id, never fetched.

Recording applies to the local platform ([Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/)) and stays on until you stop it. It covers
everything that runs there — your tests, your pages, your API calls alike.

## Examples       {#examples}

```console
osy trace start        # record the steps too
osy test               # reproduce the thing you are chasing
osy inspect            # ...then read what happened
osy trace stop         # done

osy trace start --full # ...or also capture the locals at every step, for a wrong-value bug
```

## See also       {#see-also}

[Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/) — open a run and read it (works with recording off, too).

[Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/) — the local platform that holds the traces.


---

<!-- https://osysharp.com/reference/local/running-a-function/ -->

# Running a function

> Runs one of your app's own functions from the command line — to put the app in a known state, backfill a column, or kick off a job. It runs as a real user (or as nobody), so your security rules apply exactly as they do in the browser.

<!-- id: local-running-a-function · area: local · stability: stable · html: https://osysharp.com/reference/local/running-a-function/ -->

## Summary        {#summary}

Runs one function your app declares, with arguments, against the local platform
([Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/)). It is the third thing you can do to an app from outside it: `osy compile` ships
its code, [`osy import`](https://osysharp.com/reference/local/importing-data/) loads its rows, and `osy run` makes it **do** something.

The run happens **as somebody**. With no `--as` it runs anonymous — the same thing a visitor with no session gets.
Name a user and it runs as that person, with their roles and row filters live.

## Signature      {#signature}

```console
osy run <function> [path] [--arg NAME=VALUE] [--args-json <json>] [--as <login>] [--password <pw>] [--json]
```

## Description    {#description}

### What you can run   {#what}

Any top-level function your app declares. Methods on a class need an instance, constructors are not verbs, tests
belong to `osy test`, and a workflow's entry points are driven by the workflow engine — each of those is refused by
name, telling you which it is. Misspell a function and the answer lists the ones you can run.

```console
osy run RecalculateTotals
osy run SendDigest --arg since=2026-08-01 --arg dryRun=true
```

### How do I pass arguments?      {#arguments}

`--arg NAME=VALUE` binds one parameter and repeats. `true`, `false` and numbers are read as such; everything else is
text, including identifiers and dates, which are converted to the parameter's declared type on arrival. Only the first
`=` splits, so `--arg filter=status=open` passes `status=open`.

When a parameter takes a class, pass the whole argument object as JSON:

```console
osy run PlaceOrder --args-json '{"order":{"reference":"A-1","total":42}}'
```

### Passing an entity      {#entity-arguments}

A parameter typed as one of your entities takes **a row**, and what you pass decides which row:

| you pass | it binds |
|---|---|
| an existing row's id — `--arg customer=8050d9b7-…`, or `{"customer": "8050d9b7-…"}`, or `{"customer": {"id": "8050d9b7-…"}}` | **that row**, loaded as the principal the run happens as. An id that names no row — or a row that principal may not read — is refused as *not found*, naming the entity and the id. Nothing is constructed. |
| an object of fields with **no** id — `{"customer": {"name": "Acme", "code": "acme"}}` | **a new row** in the run's unit of work, exactly as `new Customer { … }` in the body would be; it is written when the function commits. |
| an id **and** fields | refused. An id binds a row as it is; to change its fields, do so in the function. |

```osy title="a function that returns a row, and one that takes one" test app=local-running-a-function
entity Customer {
  [MaxLength(200)] string Name;
  [Unique, MaxLength(100)] string Code;
  security {
    allow read, create when IsAuthenticated || IsAnonymous;
  }
}

entity Invoice {
  [Required] Customer Customer;
  decimal Amount;
  security {
    allow read, create when IsAuthenticated || IsAnonymous;
  }
}

Customer AddCustomer(string name, string code) {
  var c = new Customer { Name = name, Code = code };
  UnitOfWork.Commit();
  return c;
}

Invoice RaiseInvoice(Customer customer, decimal amount) {
  var i = new Invoice { Customer = customer, Amount = amount };
  UnitOfWork.Commit();
  return i;
}
```

```console
osy run AddCustomer --arg name=Acme --arg code=acme --json         # answers the row, `id` included
osy run RaiseInvoice --arg customer=8050d9b7-… --arg amount=42      # binds THAT Customer
osy run RaiseInvoice --args-json '{"customer": {"id": "8050d9b7-…"}, "amount": 42}'   # the same
osy run RaiseInvoice --args-json '{"customer": {"name": "New Co", "code": "new"}, "amount": 42}'  # a new Customer, written with the Invoice
```

The id is the one `--json` gave you when the row was returned (below), or what [`osy query`](https://osysharp.com/reference/local/reading-your-data/)
shows.

### Who it runs as   {#principal}

This is the part worth reading twice, because it decides what the run is allowed to do.

- **No `--as`** — the function runs **anonymous**. If your app denies anonymous writes, the run is refused, and that
  refusal is correct: it is what a stranger hitting the same code would get.
- **`--as <login> --password <pw>`** — the function runs as that user. `<login>` is whatever your `app.Auth` binds as
  its login field, usually an email. Their roles apply and row filters bind to them, exactly as under
  [`runas`](https://osysharp.com/reference/testing/runas/) in a test. It is a real **login**: `--as` takes that user's own password, because naming a
  principal must never be enough to become one. Omit `--password` and you are prompted.

There is no switch that turns security off. A run no user could perform tells you nothing about whether your app
works, and a privileged job is served by naming a user who genuinely holds that authority — add one with
[Adding an account](https://osysharp.com/reference/local/adding-an-account/), roles and all, if the app has none yet.

```console
osy run ArchiveOldOrders --as ops@example.com --password 's3cret'
```

A login or password that does not check out refuses the run — as one answer, "invalid login or password", the same
thing your app's own login page says. It never quietly falls back to anonymous, because a run with less authority than
you asked for looks exactly like a successful one until it doesn't.

### What comes back   {#output}

A function that returns a value prints it. `--json` gives you the whole result — whether it succeeded, what it
returned, and which principal it ran as — for a script to read.

A returned **entity** is its row: `id` first, then every field you declared, then `createdAt` and `modifiedAt`. A
reference member (`Invoice.Customer`) is the referenced row's id. A returned list is one such object per row. The `id`
is the one value every row is guaranteed to have, and it is what the next call takes — you never need a function of
your own to learn it.

```json
{
  "success": true,
  "output": {
    "id": "8050d9b7-ab3b-46b1-9037-ee9631e52064",
    "name": "Acme",
    "code": "acme",
    "createdAt": "2026-09-05T21:45:26.8817050Z",
    "modifiedAt": "2026-09-05T21:45:26.8817050Z"
  },
  "error": null,
  "ranAs": "anonymous"
}
```

A function that **refused** (a validation error, a denial, a `throw` of your own) is reported with its own message and
a non-zero exit code. That is an answer about your app, not a failure to reach it.

### Against a deployed app   {#remote}

`osyrin app run <function>` is the same command against a platform you are logged in to, with the same `--as` rule —
which matters more there, not less.

## Examples       {#examples}

```console
osy run SeedCatalogue                                  # anonymous — fine if the app allows it
osy run SeedCatalogue --as ops@example.com --password 's3cret'   # as a real user, with their rules
osy run Backfill --arg batch=500 --arg dryRun=true --json
```

## See also       {#see-also}

[Importing data](https://osysharp.com/reference/local/importing-data/) — load the app's rows, the other half of putting it in a known state.

[Compiling your app](https://osysharp.com/reference/local/compiling-your-app/) — ship the code the function comes from.

[runas](https://osysharp.com/reference/testing/runas/) — the same principal idea inside a test.

[The inner loop](https://osysharp.com/reference/local/the-inner-loop/) — where this sits in the build-run-look loop.


---

<!-- https://osysharp.com/reference/local/running-a-local-platform/ -->

# Running a local platform

> Runs a full platform on your own machine — its own database, no account, no cloud, reachable only from your computer. It is the local runtime for the whole build-and-test loop: start it once, then build, test, and debug your app against it offline.

<!-- id: local-running-a-local-platform · area: local · stability: stable · html: https://osysharp.com/reference/local/running-a-local-platform/ -->

## Summary        {#summary}

Runs a complete platform on your own machine, with a database it starts and manages itself. Nothing outside your
computer can reach it, it needs no account and no cloud project, and the only difference from a hosted platform is which
address you point at. It is what [Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) and [Debugging tests locally](https://osysharp.com/reference/testing/debugging-tests-locally/) run against.

## Signature      {#signature}

```console
osyrin dev [--devname <name>] [--port <n>] [--keepalive <minutes>] [--reset]
```

## Description    {#description}

### Starting it   {#starting}

Run it from your project directory and leave it running in a terminal:

```console
osyrin dev
```

The first start downloads a small database bundle once; after that it is up in a few seconds. It binds to your machine
only, so nothing on the network can reach it. Your local data persists between runs — stop and restart and it is still
there. `--reset` throws that data away and rebuilds from your source.

A first start that is **interrupted** — stopped, or ended by another command, while it is still preparing its database —
costs nothing but the retry: the next start recognises its own unfinished work, discards it and prepares the database
again. It never refuses to start over something it left behind itself.

### One server per project, on a stable address   {#address}

Each project gets its own local platform, and it always answers on the same address — the port is derived from the
project, so a bookmark keeps working across restarts and two projects (or two checkouts of the same repo) never fight
over a port. `--port` overrides that when you need a specific one. Because the address is derived, the other commands
find the running server on their own; you never have to tell them where it is.

Starting `osyrin dev` again for the same project **reclaims** the one already running rather than leaving it stranded:
there is only ever one server per project, and a restart takes the place of the old one instead of piling up beside it.

`--devname <name>` gives the server an explicit identity instead of deriving it from the project directory — its data,
its address and its one-per-identity reclaim all follow the name. Use it to run a second, separately-named platform
from the same directory, or one shared platform from several. Every other command takes the same `--devname`, so point
`compile`, `test`, `launch` or `logs` at a named server with the flag, or set `OSY_DEVNAME` once for the whole shell.

A name is the **only** way to say which platform you mean: a command never takes a data directory, so the path is always
derived from the identity. That is deliberate — when two commands name the same identity they cannot reach different
platforms, so a compile can never quietly land somewhere other than the app you are looking at.

### It stops itself when you walk away   {#idle-exit}

By default a server that has gone **60 minutes with no requests exits on its own**, so an abandoned one does not keep a
whole platform and its database running until you notice. When it does, it says so plainly in its output — that it
exited because it was idle, and how to change the window — so a server that is gone when you come back is never a
mystery. `--keepalive <minutes>` sets the window; `--keepalive 0` turns the behaviour off and the server runs until you
stop it. An **attached debugger keeps it alive**: a paused breakpoint sends no requests, but the server will not be
reaped out from under you.

### Or let the loop start it for you   {#autostart}

You do not have to start it by hand. When you run [Compiling your app](https://osysharp.com/reference/local/compiling-your-app/), [Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/), or
[Launching your app](https://osysharp.com/reference/local/launching-your-app/) and no local platform is running for the project, they start one for you in the background,
wait for it to be ready, and leave it running so the next command is fast. The whole loop is one command — you never
have to remember to start (or restart) a server. When you're done, [Stopping the local platform](https://osysharp.com/reference/local/stopping-the-local-platform/) shuts it down;
its database stops with it, so nothing is left running.

A server keeps the build it started with, so after you rebuild the platform these commands **replace** an older server
rather than talk to it. Several commands arriving at once — `compile` and `test` fired together, say — produce **one**
restart: the first to arrive does it, and the others wait for the new server and say so, however long a first start
takes on a busy machine. The same holds for a server that is still starting when a command arrives, whoever started
it: the command waits for it instead of starting a second one beside it.

### You are the administrator — no login   {#no-login}

On a machine-only platform the person at the keyboard is the administrator, so the operator commands need no
credentials: `login`, `whoami` and `connect` simply report that and continue. This applies **only** to the platform's
own operator commands. Your application's security is unchanged and behaves exactly as it does in production — a test
runs as an anonymous, secured caller, your app's own login works, and your access rules are enforced. Local is faster
and private; it is never a relaxed rulebook.

### An organization is already there   {#organization}

A local platform comes with a single ready-made organization, so there is no organization-and-user dance before you can
create an app. `org list` shows that one organization and `org create` reports it (there is nothing to create locally).
Apps you create belong to it, and are reachable by their plain name — an app named in your project is served at
`http://<app>.localhost:<port>/`.

### What works, and what says so when it can't   {#coverage}

Everything the local loop needs works: validate and build, run and debug tests, create and compile an app, launch it,
manage its users, read its logs, and set its secrets. A handful of commands describe operations a machine-only platform
cannot perform — taking cloud backups, publishing or deploying a release, and the like. Those **fail loudly with a
clear reason and a non-zero exit**; they never report success for something that did not happen. If a command can't do
the thing here, it tells you plainly.

### It is for development, not hosting   {#not-hosting}

A local platform is a fast, private place to build — not a place to run something for real. It has no backups, no
sharing, and no durability, and it is deliberately unsuitable for hosting. Durability lives on a hosted platform; keep
anything that matters there.

## Examples       {#examples}

The everyday loop — one platform, many runs, all offline:

```console
# terminal 1
osyrin dev

# terminal 2
osy test
osy test --filter Totals
```

Reset the local data and start fresh:

```console
osyrin dev --reset
```

Keep a server running with no idle timeout, on a port you choose:

```console
osyrin dev --keepalive 0 --port 8099
```

Run a second, separately-named platform from the same project, and point the other commands at it:

```console
# terminal 1
osyrin dev --devname scratch

# terminal 2
OSY_DEVNAME=scratch osy compile
```

## See also       {#see-also}

[Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) — run your app's tests against this local platform.

[Debugging tests locally](https://osysharp.com/reference/testing/debugging-tests-locally/) — debug a single test against it, with breakpoints in your editor.

[Launching your app](https://osysharp.com/reference/local/launching-your-app/) — open your app in a browser on this local platform.

[Compiling your app](https://osysharp.com/reference/local/compiling-your-app/) — compile your source into your app on this local platform.

[Stopping the local platform](https://osysharp.com/reference/local/stopping-the-local-platform/) — stop the local platform serving your project.


---

<!-- https://osysharp.com/reference/local/seeing-what-a-page-cost/ -->

# Seeing what a page cost

> Every call your app makes to the local platform reports how many database round trips answered it, beside how long it took. A duration alone cannot tell one query from nineteen, and that difference is usually the thing to fix.

<!-- id: local-seeing-what-a-page-cost · area: local · stability: stable · html: https://osysharp.com/reference/local/seeing-what-a-page-cost/ -->

## Summary        {#summary}

The dev panel's **Net** tab shows, for every call your app made: how long it took **and what the server did to answer
it** — the database round trips, the statements they carried, and how much of the time was spent waiting on SQL.

A millisecond number on its own cannot tell you whether 340ms was one query or nineteen, and that is usually the
question worth asking. The count makes it visible without having to suspect it first.

## Signature      {#signature}

```text
Ctrl+Shift+D  →  Net           the panel, on any page served by the local platform
window.__osy.net                the same rows as data, for a script or an agent
```

Each row reads:

```text
GET   200   /api/data/Contact       41ms   19rt/27q   23ms sql   9f3c…
                                    └ wall  └ round     └ of which  └ correlation id
                                      clock   trips       waiting
                                              /statements on SQL
```

## Description    {#description}

**`19rt` is the number: NETWORK ROUND TRIPS.** Not statements, and the two are not the same count — the platform
pipelines, so a batch of forty statements is **one** trip. The row shows the statements alongside whenever they
differ (`1rt/40q`), because that gap *is* the batching and `1rt` on its own reads better than it deserves.

⚠ **Do not call either of them "queries".** Round trips are what latency is made of and what this page is about;
the statement count is a different question, and a read-in-a-loop that got batched hides in the first and shows in
the second.

**`23ms sql` tells you which of two problems you have.** Close to the row's total: the time is round-trip *latency*,
and the fix is fewer, bigger queries. Far below it: the time went somewhere else — CPU, or waiting on something that
is not the database — and the query count is not your bug however large it looks.

**A count is only meaningful against what the call was for.** Nineteen queries to paint a report page is ordinary;
nineteen behind a button press is usually a read inside a loop. The panel tints a high count as a nudge to look, not
as a verdict.

**The query shapes are deliberately not here.** They are in the server log, under the correlation id on the same row:

```bash
make logs-corr ID=9f3c1a2b-…
```

That line carries the top shapes by frequency *and* by rows — the second ranking matters because one unfiltered read
of a big table is a single round trip and never appears in a by-frequency list, while being most of the work. Keeping
one copy of the shapes is why the panel shows the number and the log shows the detail.

**Local dev host only.** The `X-Osy-Db` header this reads is emitted only by a platform started with `osy launch` /
`osyrin dev`, on the same gate as the panel itself. A deployed host sends nothing: in production a query-shape
description is free information to anyone with a browser, for the benefit of nobody. On a host that does not send it
the field reads as **not measured**, never as zero.

**What it counts is the request, all of it** — everything the call awaited, including work in functions it invoked.
The one thing outside the number is a query issued *while the response body is being written*; nothing in the
platform does that today (rows are materialised before they are serialised).

## Examples       {#examples}

Find the page that got slow, from a script:

```js
// syntax — the dev handle is a browser global, not Osy# source.
window.__osy.net
  .filter((c) => c.db && c.db.net >= 10)
  .map((c) => `${c.method} ${c.url} — ${c.ms}ms, ${c.db.net} queries, ${c.db.sqlMs}ms sql`);
```

Then read the shapes behind the worst one:

```bash
make logs-corr ID=<the correlation id on that row>
```

## See also       {#see-also}

- [Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/) — `osy inspect`, for what the code did rather than what it cost
- [Recording a run](https://osysharp.com/reference/local/recording-a-run/) — `osy trace`, when you need the steps and not the totals
- [Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/) — the host that emits this


---

<!-- https://osysharp.com/reference/local/seeing-what-happened/ -->

# Seeing what happened when your code ran

> Opens a finished run and shows what it did — each step named and pointed at its source line, the values that crossed between client and server, and, when it threw, the exact place and the values in scope there. You do not have to have been recording, and you do not have to reproduce the failure.

<!-- id: local-seeing-what-happened · area: local · stability: stable · html: https://osysharp.com/reference/local/seeing-what-happened/ -->

## Summary        {#summary}

Answers the question you actually have when something goes wrong: **what happened when it ran?** A run that threw is
retained with the stack and the values as they were **at the moment of the throw** — so instead of adding logging and
running the failure a second time, you open the failure you already have.

## Signature      {#signature}

```console
osy inspect [traceId] [--list] [--fault] [--step N] [--correlation-id <id>] [--json]
```

## Description    {#description}

Something failed — a test went red, a page action broke, a tool call came back with an error. The message tells you
*that* it threw. It does not tell you **why**, because the message does not carry the values.

`osy inspect` does. Run it with no arguments and it lists the runs it has kept, the faulted ones marked with the
exception that ended them. Give it a run and it opens that run.

**Every step is named and points at your source.** A step is not an opaque position — it shows the function it ran, the
file and line it was on, and the statement itself. A step lands where you would put your cursor, in the `.osy` you wrote.

**A run that crosses client and server reads as one story.** A page action that awaits a server function — or a
server function that calls back to the client — interleaves both sides in one sequence, and shows the values that
**crossed** between them: the arguments handed over (`→ handoff`) and the value handed back (`← resume`). "Why did the
server receive the wrong argument?" and "why did the page get the wrong result?" become answerable without guessing at
the boundary.

**You did not have to predict the failure.** A fault is captured whether or not you had recording on: it costs nothing
until something actually throws, so the moment you most need the state is never the moment it is discarded. Turning
recording on (see [Recording a run](https://osysharp.com/reference/local/recording-a-run/)) additionally records the *steps that led there* — but you never need it
just to see where and why a run died.

**The values are the callee's.** A throw is almost never in the function you invoked; it is several calls down, and the
state that explains it belongs to the function that threw — the arguments it was called with, and the local that made
the guard trip. `--fault` shows that stack, innermost frames included.

**For a wrong result rather than a crash, record with `--full`.** A fault shows the values at the throw; a run that
finished with the *wrong answer* never threw. Record it with `osy trace start --full` ([Recording a run](https://osysharp.com/reference/local/recording-a-run/)) and
each step carries the locals in scope at that point — so you watch the value that ends up wrong take shape, step by
step, on both sides of the run.

**Nothing is resolved for you.** A reference to a record is shown as its type and id, never fetched — reading it would
mean a database query under somebody's permissions, and a post-mortem view must not do that. A string is shown quoted,
so `"3"` is visibly not `3`.

**It is local only.** Traces are kept in the local platform you run while developing ([Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/))
and never leave your machine. A deployed server does not record them and will tell you so if asked.

Traces are scoped to *this* project's app, so a workspace of several apps does not mix their runs. Only the most recent
runs are kept; a faulted run is kept in preference to a successful one, so a failure is not pushed out by the green runs
behind it. If a run was long enough to be truncated, it says so rather than presenting a partial story as a whole one.

**You do not need the run's own id to find it.** When something failed through your app — an error dialog, a log line —
you usually hold a **correlation id**, the id of the interaction, not of one run. `--correlation-id <id>` opens the set
of runs that one interaction caused (a single click can trigger several), so you go from "this failed" to "these are
the runs it produced" without hunting for a trace id.

`--json` writes the trace as JSON — the form to hand to a coding agent, with the names, source spans, crossed-wire
values and locals it needs to act against your source.

This is a **read** of a finished run. To pause a *live* one and step it, use the debugger instead.

## Examples       {#examples}

```console
osy inspect                          # what runs are retained? which one broke?
osy inspect <traceId> --fault        # where it threw, and the values that were in scope there
osy inspect <traceId>                # the whole run
osy inspect <traceId> --step 12      # one recorded step
osy inspect --correlation-id c-9f2a  # the runs one interaction (a click) caused
osy inspect <traceId> --json         # the same, as JSON
```

A test fails. The message says only `ValidationException: discount must be under 100%`. The fault says why — named, at
the source line, with the values in scope:

```console
$ osy inspect 232a6a2c --fault
FullDiscount_IsRejected  Faulted  232a6a2c-ec59-402a-ab53-7d6747687c4b

ValidationException: discount must be under 100%

  ApplyDiscount  at pricing.osy:14
    total = 200
    percent = 100
    factor = 0
```

`percent` came in as `100`, so `factor` computed to `0` and the guard tripped. Nobody was recording.

A checkout returns the wrong total — no crash, just a wrong number. Recorded with `--full`, the whole run reads as one
story: each step at its source line, the values crossing between the page and the server, and the locals taking shape:

```console
$ osy trace start --full
$ osy inspect 7c1d0a4e
Cart.CheckOut  Completed  7c1d0a4e-...

    0  client stmt    CheckOut  at checkout.osy:8   total = Price(cart);
    1  → handoff  Price(cart: [Item#a1, Item#b2])
    2  server stmt    Price     at pricing.osy:4    var sum = 0;
         sum = 0
    3  server return  Price     at pricing.osy:9    return sum;
         sum = 240
    4  ← resume   Price returned 240
```

The arguments that crossed to the server, the value that came back, and `sum` at each step — enough to see exactly
where 240 should have been something else.

## See also       {#see-also}

[The inner loop](https://osysharp.com/reference/local/the-inner-loop/) — where `osy inspect` sits in the develop-and-debug loop.

[Recording a run](https://osysharp.com/reference/local/recording-a-run/) — also record the steps that led to a failure.

[Log.*](https://osysharp.com/reference/diagnostics/log/) — what your app writes with `Log.*`; a failure's log line names the run to inspect.

[Understanding your app](https://osysharp.com/reference/local/understanding-your-app/) — what the app *is*, as opposed to what it did.


---

<!-- https://osysharp.com/reference/local/stopping-the-local-platform/ -->

# Stopping the local platform

> Stops the local platform serving your project — the one the build-and-test loop starts on demand. Its database stops with it, so nothing is left running in the background.

<!-- id: local-stopping-the-local-platform · area: local · stability: stable · html: https://osysharp.com/reference/local/stopping-the-local-platform/ -->

## Summary        {#summary}

Stops the local platform for your project — the one [Compiling your app](https://osysharp.com/reference/local/compiling-your-app/), [Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) and
[Launching your app](https://osysharp.com/reference/local/launching-your-app/) start on demand. The platform's database stops with it, so there is nothing left running
after it returns.

## Signature      {#signature}

```console
osy stop [path] [--devname <name>]
```

## Description    {#description}

The loop commands start a local platform in the background when one isn't already running, and leave it up so your next
command is fast ([Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/)). `osy stop` is how you shut that platform down when you're done —
it stops the server and the database it manages, and reports plainly when there was nothing running.

You rarely need it during a normal session: leaving the platform running between commands is the point, and it costs
little. Reach for it when you want a clean slate, are switching away from a project, or want to be sure nothing is
running in the background.

- `path` selects the project (defaults to the current directory).
- `--devname` stops a named server instead of this project's, matching `osyrin dev --devname` (setting `OSY_DEVNAME`
  works too).

It only ever stops a server it can confirm is the one it started: it matches the running process against what the
server recorded about itself, and if the recorded server is already gone it simply reports that and stops nothing — it
will not signal an unrelated process that has since taken over the same slot.

## Examples       {#examples}

```console
osy stop            # stop the local platform for this project
```

## See also       {#see-also}

[Listing your local platforms](https://osysharp.com/reference/local/listing-local-instances/) — see every local platform on this machine, and which are still running.

[Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/) — the local platform this stops, and how the loop starts it for you.

[Compiling your app](https://osysharp.com/reference/local/compiling-your-app/) — compile your source into your app (starts the platform if needed).

[Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) — run your app's tests locally (starts the platform if needed).


---

<!-- https://osysharp.com/reference/local/the-inner-loop/ -->

# The inner loop

> The local develop-and-debug loop for an Osy# app — for the person in the editor and the coding agent at the CLI alike. Run your app on a local platform, see exactly what a run did when it misbehaves, and check it before you ship, all against the source on your disk.

<!-- id: local-the-inner-loop · area: local · stability: stable · html: https://osysharp.com/reference/local/the-inner-loop/ -->

## Summary        {#summary}

Everything you do to an Osy# app while building it happens in one short loop: change the source, run it on a local
platform, and — when it does the wrong thing — see exactly what it did and why. The same commands serve a person typing
in the editor and a coding agent working at the command line: both edit the same `.osy` files, run the same local
platform, and read the same explanations of what happened, so an agent debugging your app sees precisely what you would.

## Signature      {#signature}

```console
osyrin dev     # a local platform to run on
osy launch     # compile the current source and open it
osy inspect    # see what a run did — named, at its source line, with the values
osy lint       # is it production-ready?
```

## Description    {#description}

The loop has four moves. You rarely do all four every time — most turns are *edit → launch → look* — but this is the
whole of it.

### Run it            {#run}

A local platform hosts your app while you build. Start one with `osyrin dev` ([Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/)); it runs
only on your machine and needs no account. `osy launch` ([Launching your app](https://osysharp.com/reference/local/launching-your-app/)) compiles the source on your disk
into the app and opens it in a browser — edit, launch, see the result. When the platform is already running, an open
page even updates itself as you recompile, so the tightest loop is just *save and look*.

Your app runs the **real** thing locally: its own login, its own access rules, the same compiler that runs in
production. What opens is what you wrote, not a stub.

An app is more than its code, so two commands put yours in a state worth looking at: `osy import`
([Importing data](https://osysharp.com/reference/local/importing-data/)) loads the rows it ships with, and `osy run` ([Running a function](https://osysharp.com/reference/local/running-a-function/)) runs one of its
own functions — to seed something, backfill a column, or kick off a job — as a real user or as nobody.

### See what it did   {#see}

When a run misbehaves, you do not add logging and run it again — you open the run you already have. `osy inspect`
([Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/)) shows a finished run as one readable story:

- **Named, and pointed at your source.** Every step shows the function it ran, the file and line, and the statement —
  so a step lands where you would put your cursor.
- **Across the client↔server boundary, one story.** A page action that awaits a server function reads as a single
  sequence, showing the values that **crossed** the wire — the arguments handed over and the value handed back.
- **The failure, for free.** A run that threw is kept with the exact place it threw and the values in scope there,
  whether or not you were recording — so the moment you most need the state is never the moment it is discarded.
- **The steps that led there, on demand.** Turn on recording ([Recording a run](https://osysharp.com/reference/local/recording-a-run/)) to also get the path a run
  took; add `--full` to capture the locals at **every** step — for a wrong *result* rather than a crash.

When what you hold is not a run's id but the id of an *interaction* (from an error dialog or a log line), `osy inspect
--correlation-id <id>` opens the set of runs that one click caused.

### Check it before you ship   {#check}

`osy lint` ([Checking your app](https://osysharp.com/reference/local/checking-your-app/)) reads the app the way a reviewer would and reports what is not
production-ready — before it becomes a live problem. Run it as the last step of the loop, and in whatever runs your
changes automatically.

### What IS this app? `osy model`   {#understand}

Sometimes the question is not what a run *did* but what the app *is* — its entities, pages, functions and how they
connect. `osy model` ([Understanding your app](https://osysharp.com/reference/local/understanding-your-app/)) answers that: the shape of the whole app, in a form a person or
an agent can read.

### The same loop, for a coding agent   {#for-agents}

Every command above has a machine-readable form (`--json`), and that is deliberate: the loop is the SDLC for a coding
agent as much as for you. An agent editing your app runs the same local platform, inspects the same runs, and checks
the same rules — and because a trace speaks in the names, source lines and values of the code the agent is holding, an
explanation lands directly in the `.osy` it is editing, not in terms it would have to reverse-engineer.

## See also       {#see-also}

[Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/) — start the local platform the loop runs on.

[Launching your app](https://osysharp.com/reference/local/launching-your-app/) — compile the current source and open it in a browser.

[Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/) — open a finished run and read what it did.

[Recording a run](https://osysharp.com/reference/local/recording-a-run/) — also record the steps, and (with `--full`) the locals at each step.

[Checking your app](https://osysharp.com/reference/local/checking-your-app/) — what is not production-ready yet.

[Understanding your app](https://osysharp.com/reference/local/understanding-your-app/) — the shape of the app itself, as opposed to what it did.


---

<!-- https://osysharp.com/reference/local/index/ -->

# The local loop (running your app on your own machine)

> Everything the `osy` CLI does to the project in front of you — run it, look at what it did, check it before you ship. The first-day mistake is treating a local platform as a database you can leave data in: a compile mints a new app version and yesterday's rows are not what the new one reads, so seed through the app's own functions or `osy import` after every compile.

<!-- id: local-index · area: local · stability: stable · html: https://osysharp.com/reference/local/index/ -->

## Summary        {#summary}
**`osy` is the local surface: it works on the project in front of you, needs no account and no cloud.** Most of it
is offline. It runs against a local platform it starts for you on demand, and the whole area is one loop — write,
run, look, check.

```bash title="the loop, in four verbs" syntax
osy launch      # create the app if missing, compile this source into it, open it
osy logs --tail # what it said — client and server lines under one correlation id
osy inspect     # what actually happened in a run, and where it faulted
osy check       # validate + lint + test, one verdict, before you ship
```

## Description    {#description}
**A compile mints a new app VERSION, and data does not carry across it.** This is the fact that surprises people
first: a recompile is not an edit in place, so rows you created against the previous version are not what the new
one reads. Seed through the app's own functions, or [Importing data](https://osysharp.com/reference/local/importing-data/), after a compile — never by expecting
yesterday's data to still be there.

**A local platform is a cache, not a store.** Instances live under `~/.osy/instances/`, each carrying its own
Postgres cluster, and they are reaped: one you have not touched for a day is gone, and that is correct — losing one
costs a rebuild. [Listing your local platforms](https://osysharp.com/reference/local/listing-local-instances/) shows every instance on the machine and which are running;
[Reclaiming the disk your local platforms use](https://osysharp.com/reference/local/reclaiming-local-disk/) is the deliberate sweep. [Stopping the local platform](https://osysharp.com/reference/local/stopping-the-local-platform/) ends the one serving
this project.

**Ask the platform rather than reading the compiler.** Most questions about *your app* have a verb:
[Understanding your app](https://osysharp.com/reference/local/understanding-your-app/) for the resolved model, [Explaining your app's security](https://osysharp.com/reference/local/explaining-your-app/) for the declared security in
plain English, [Reading a capability's source](https://osysharp.com/reference/local/reading-a-capability/) for what a surface offers. A question you answer by reading source is
a question the platform has not answered yet.

**When something misbehaves, look before you reason.** [Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/) opens the trace and jumps to
the fault; [Recording a run](https://osysharp.com/reference/local/recording-a-run/) captures one deliberately; [Seeing what a page cost](https://osysharp.com/reference/local/seeing-what-a-page-cost/) answers the
performance question. Faults are captured even with tracing off.

## The pages      {#the-pages}
Run `osy docs local` for the full listing.

- **The loop itself** — [The inner loop](https://osysharp.com/reference/local/the-inner-loop/), [Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/),
  [Stopping the local platform](https://osysharp.com/reference/local/stopping-the-local-platform/), [Installing the editor extension](https://osysharp.com/reference/local/installing-the-editor-extension/), [Upgrading the toolchain](https://osysharp.com/reference/local/upgrading-the-toolchain/)
- **Getting your app running** — [Launching your app](https://osysharp.com/reference/local/launching-your-app/), [Launching the page you're on](https://osysharp.com/reference/local/launching-the-page-youre-on/),
  [Compiling your app](https://osysharp.com/reference/local/compiling-your-app/), [Adding an account](https://osysharp.com/reference/local/adding-an-account/), [Giving a secret its value](https://osysharp.com/reference/local/giving-a-secret-its-value/)
- **Getting data in and out** — [Importing data](https://osysharp.com/reference/local/importing-data/), [Running a function](https://osysharp.com/reference/local/running-a-function/),
  [Reading your data](https://osysharp.com/reference/local/reading-your-data/)
- **Looking at it** — [Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/), [Recording a run](https://osysharp.com/reference/local/recording-a-run/),
  [Seeing what a page cost](https://osysharp.com/reference/local/seeing-what-a-page-cost/)
- **Asking about it** — [Understanding your app](https://osysharp.com/reference/local/understanding-your-app/), [Explaining your app's security](https://osysharp.com/reference/local/explaining-your-app/),
  [Reading a capability's source](https://osysharp.com/reference/local/reading-a-capability/)
- **Before you ship** — [Checking your app](https://osysharp.com/reference/local/checking-your-app/), [What a clean check means](https://osysharp.com/reference/local/what-a-clean-check-means/)
- **Housekeeping** — [Listing your local platforms](https://osysharp.com/reference/local/listing-local-instances/), [Reclaiming the disk your local platforms use](https://osysharp.com/reference/local/reclaiming-local-disk/)

## See also   {#see-also}
- [The inner loop](https://osysharp.com/reference/local/the-inner-loop/) — the loop end to end, for a person and for an agent at the CLI
- [Launching your app](https://osysharp.com/reference/local/launching-your-app/) — create, compile and open in one verb
- [What a clean check means](https://osysharp.com/reference/local/what-a-clean-check-means/) — what a green `osy check` does and does not prove


---

<!-- https://osysharp.com/reference/local/understanding-your-app/ -->

# Understanding your app

> Prints your app's RESOLVED model — field types bound to real types, relations wired to the entity they target, each entity's actual security posture, what every function reads, writes, calls out to and raises, and the REST surface the app publishes to the outside.

<!-- id: local-understanding-your-app · area: local · stability: stable · html: https://osysharp.com/reference/local/understanding-your-app/ -->

## Summary        {#summary}

Prints what your app **is** — not what its source says, but what the compiler makes of it. Types are bound, relations
are wired to the entity they actually point at, every entity reports the access posture it really has, and every
function reports what it does when you call it. It needs no platform and no database: it parses and resolves, nothing
else.

## Signature      {#signature}

```console
osy model [path] [--json]
```

## Description    {#description}

Reading source tells you what a field is *called* and what its type is *spelled*. `osy model` tells you what those
resolve to — which is where the surprises live:

- **Relations are wired.** A `LineItem[] Lines` field reports its target: a collection of `LineItem`. A `Order Order`
  field on the child reports a reference back. You never have to infer a relation from a name.
- **Enum-backed fields are named by their enum**, not by the string or int they store as — because the enum is what
  you have to write.
- **The rules a field is held to are listed.** `[Unique]`, `[Min]`, `[Max]`, `[Pattern]`, `[MaxLength]`,
  `[Immutable]` and friends are reported per field under `constraints`, each named as you write it, with its
  argument. This is how you tell a rule about the **data** from a check in one screen: a constraint holds wherever
  the row is written — a function, an import, another tab — while an `if` in a page holds only in that page. A
  `[Unique(A, B)]` written on the entity is a rule about the *combination* and belongs to no single field, so it is
  reported on the entity instead. (`[Required]` is not repeated in that list — it has its own `required` flag.)
- **Security is the DERIVED posture, not the block.** An entity with no `security { }` block is **denied to everyone**
  (see [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/)) — not open, and there is no setting that could make it open. That is the fact
  people most often read backwards, so the model reports the derived posture (`deny-all`, `allow`, `deny`) rather
  than leaving you to work it out from whether a block is present.
- **Functions report their effects.** What each one reads, creates, modifies, deletes, calls out to, raises, and
  throws — derived from the body, not from the name. Each function is reported twice: its **own** effects, and its
  **transitive** ones (what happens once its callees are included). A function that writes nothing itself but calls
  one that does will tell you so.

- **A workflow reports the machine, not just its parts.** States and events on their own describe a workflow the
  way a cast list describes a play. The model also reports what moves between the states, and — because this is
  where the surprises live — the things that quietly decide an outcome: which states **accrue** SLA time (a state
  left out of `Accrues` pauses every clock while a run sits in it), the schedule those clocks run on, each slot's
  `Candidates` and `Requires` gates, and each milestone's promise, reminders and retry policy.

  One link is worth knowing about. You *raise* an event, but a state *subscribes* it under a slot alias, and the
  alias is what handlers and transitions use — so `Respond` and `FirstReply` can be the same act under two names.
  Each event reports where it can be delivered, and each transition reports the event that drives it, so you never
  have to reconcile two lists of identifiers by hand.

- **The REST surface you publish is reported, with the columns it hands out.** Every `RestApi` in `app.Apis`
  appears under `apis`: the address it is really served on (`/api/rest/v{major}/{route}` — with the default version
  applied, so it is where the API *is*, not what the source spelled), the credential kinds it accepts, each function
  you mapped to a URL, and each entity you exposed. This is the one part of your app that faces strangers, so it is
  reported at the level of detail that decides whether something leaks:

  - **No declared `Auth` means anonymous**, and the model says so in words rather than by an empty list. An
    unauthenticated API is the whole story about that API, and it is the easiest thing in the document to read past.
  - **A `Crud<T>` selects no subset.** It publishes T's whole row — your fields *and* the ones the platform adds —
    so the model lists them by name under `fields`. A cost column or a private note is on the wire, and the
    declaration that put it there never mentions it.
  - **A field-scoped `deny read` is the one thing that takes a column back off**, so those are listed separately
    under `maskedFields`. A field that appears in `fields` and not in `maskedFields` is handed to every caller the
    API admits.

- **The tables you did not write are reported too, separately.** `entities` is what your source declares.
  `provided` is what your app *has* without declaring it: everything a `using` brought in, plus the platform's
  always-applied core baseline. They are kept apart because "what did I write" and "what tables does this app have"
  are different questions — but a file-manager app whose whole subject is `FileAsset` was reporting five entities
  and mentioning none of them. Each provided type names the `using` it came from, and is flagged `internal` when the
  app cannot name it at all (so no `partial entity` can state security for it). Read one with
  [Reading a capability's source](https://osysharp.com/reference/local/reading-a-capability/).

The model says when it does not know. If the source did not fully resolve, `resolved` is `false` and the model is
**partial** — it is not presented as the whole truth. If a function contains something the effect analysis could not
classify, it reports `unknown: true` alongside whatever it did find, rather than quietly reading as "no effects".

`--json` writes the whole model as JSON — the form to hand to a coding agent, or to diff between two revisions of an
app. Exit is non-zero when the model is partial.

For the shorter question "what names exist here?", `osy symbols` lists them (`osy symbols` alone reads the current
project, like every local verb) — each name under the group it was DECLARED in: `Entities`, `Classes`, `Enums`,
`Functions`, `Workflows`, `Clients`, `Values`. A workflow is listed by its own name with what it tracks
(`InvitationFlow (tracks Invitation.Status)`), never as the entity it tracks, and a `class` is not an entity. A
`[Test]`/`[TestFixture]` function is under `Tests`, not `Functions` — it is the app's test-framework surface, not
production code — and a synthesized asset vocabulary (`Icons`, `Art`, `Textures`, `Sounds` — present in EVERY app,
since the built-in icon set is always merged in) is under `Assets`, not `Enums`, because the compiler wrote it from
the files the app ships rather than the app declaring it. For "where does this app fall short of production?", see
[Checking your app](https://osysharp.com/reference/local/checking-your-app/).

## Examples       {#examples}

```console
osy model                 # the resolved model, human-readable
osy model --json          # the same model as JSON
```

A workflow's slot answers "how do I drive this?" — the event to raise, who may raise it, and what must be true
first:

```json
{ "event": "Resolve", "alias": "Fix",
  "candidates": "u => u.Team == Team.Support",
  "requires": [ { "name": "RootCause",
                  "must": "!Text.IsEmpty(this.Item.RootCause)",
                  "message": "Record the root cause before resolving." } ] }
```

What a published API hands out — the address, the credential kinds, and the columns an exposure puts on the wire:

```json
{ "name": "Public", "route": "public", "version": "2.0",
  "basePath": "/api/rest/v2/public",
  "auth": { "methods": ["apiKey"] },
  "expose": [ { "entity": "Order", "operations": ["read", "create"],
                "url": "/api/rest/v2/public/entities/Order",
                "fields": ["Reference", "Cost", "PrivateNotes", "Id", "CreatedAt", "ModifiedAt", "CreatedBy", "ModifiedBy"],
                "maskedFields": ["Cost"] } ],
  "endpoints": [ { "function": "Restock", "method": "POST", "path": "/restock",
                   "url": "/api/rest/v2/public/restock", "successStatus": 201 } ] }
```

A function's effects read like this — `PlaceOrder` writes nothing itself, but calling it does:

```console
function PlaceOrder(string code) → void
  reads Customer · creates Order · calls out to Shipping.CreateShipment
```

## See also       {#see-also}

[Checking your app](https://osysharp.com/reference/local/checking-your-app/) — check the app against production best-practice rules.

[Explaining your app's security](https://osysharp.com/reference/local/explaining-your-app/) — the same app's access rules, in plain English (who can do what).

[Compiling your app](https://osysharp.com/reference/local/compiling-your-app/) — compile the source into your local app.

[secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — why an entity with no security block grants nothing.
- [Reading a capability's source](https://osysharp.com/reference/local/reading-a-capability/) — read the source of a capability the model says you were provided


---

<!-- https://osysharp.com/reference/local/upgrading-the-toolchain/ -->

# Upgrading the toolchain

> Replaces the osy you are running with a newer release — downloaded, checked against the release's checksums, and swapped in place — on every platform, with one command. `osy whats-new` prints what changed first, and the toolchain tells you, once a day, when there is something to upgrade to.

<!-- id: local-upgrading-the-toolchain · area: local · stability: preview · html: https://osysharp.com/reference/local/upgrading-the-toolchain/ -->

## Summary        {#summary}

`osy upgrade` fetches the newest release for your platform, verifies it against the checksums the release
publishes, and swaps it into the directory the running `osy` lives in. It works the same on macOS, Linux and
Windows, which the install script does not. `osy whats-new` prints a release's notes in the terminal, and the
toolchain prints one line a day when a newer version exists, ending with both commands.

## Signature      {#signature}

```console
osy upgrade                # to the newest release
osy upgrade 0.9.2          # to a particular version
osy upgrade --check        # only say whether a newer one exists
osy whats-new              # the newest release's notes
osy whats-new 0.9.2        # a particular version's
```

## Description    {#description}

**Where it comes from.** The download goes through the same door every install uses, so it is counted like any
download, and the archive is checked against the `SHA256SUMS` the release carries before a byte of it is
installed. A download that does not match is deleted and nothing changes. The version check and the notes come
from the public release page, so neither is a statistic and neither reaches us.

**How it swaps.** The files in the install directory are moved into a `.old/` folder beside them and the new files
moved in — a rename, never a copy over a running program, which is what makes it safe to run from the very
binary being replaced. The next run of `osy` removes the `.old/` folder. If anything fails during the swap the
old files are moved back.

**Your local platforms are replaced, not upgraded.** A compiled app and its embedded database belong to the
compiler that made them — the platform model and the browser runtime both move between versions — so after the
swap every local platform this installation started is stopped and removed, and the next `osy launch` or
`osy check` starts a fresh one and recompiles your app. Data you seeded into a local platform is gone with it:
these are development servers, never local production, and [Reclaiming the disk your local platforms use](https://osysharp.com/reference/local/reclaiming-local-disk/) removes the same
directories on idleness. Your source, of course, is untouched.

**The editor extension is a thin client of the toolchain** — the language server is `osy lsp`, hosted in the
binary you just upgraded, so the editor is on the new compiler the moment it restarts the server. The extension
itself is versioned with the toolchain; if an editor on your PATH has it at the previous version, the upgrade
offers to update it, or names `osy vscode install` when it cannot ask ([Installing the editor extension](https://osysharp.com/reference/local/installing-the-editor-extension/)).

**What it refuses.** An installation that a package manager owns — Homebrew's, winget's — because swapping files
under a manager leaves it wrong about what is installed; the message names the manager's own upgrade command.

**The daily line.** When a newer release exists the toolchain prints, before a command's own output and at most
once a day, a line naming the version and the two commands. It is on whether or not usage statistics are;
`"updateCheck": false` in `~/.osy/config.json` turns it off. See [Usage statistics (what is sent, and the two ids that let us count you once)](https://osysharp.com/reference/project/statistics/) for what the check sends
(a request to the release page with the toolchain's user agent, and nothing else).

## Examples       {#examples}

```console
$ osy check
a newer osy is available: 0.9.2 (you have 0.9.1) — osy upgrade
  what's new: osy whats-new   ·   https://github.com/osysharp/cli/releases/tag/v0.9.2
…
$ osy whats-new
osy 0.9.2 (newer than yours)

- …

$ osy upgrade
asking the release channel for the newest version…
downloading osy 0.9.2 for darwin-arm64…
checking it against the release's SHA256SUMS…
unpacking…
installing into /Users/you/.osy/bin…
✓ osy 0.9.2 installed (9 entries in /Users/you/.osy/bin).
  what changed: osy whats-new
  2 local platform(s) from 0.9.1 removed (1 stopped first) — the next osy launch or osy check starts a fresh one and recompiles.
  code has the Osy# extension at 0.9.1; update it to 0.9.2 now? [Y/n]
```

## See also       {#see-also}

[Installing the editor extension](https://osysharp.com/reference/local/installing-the-editor-extension/) — the extension that ships with each release.

[Usage statistics (what is sent, and the two ids that let us count you once)](https://osysharp.com/reference/project/statistics/) — what the toolchain sends, and what the version check does not.


---

<!-- https://osysharp.com/reference/local/what-a-clean-check-means/ -->

# What a clean check means

> `osy validate` checks your source without a server or a database, which is what makes it fast — and what puts a handful of checks out of its reach. A clean run now says which ones those are, so a local green never has to be read as more than it is.

<!-- id: local-what-a-clean-check-means · area: local · stability: preview · html: https://osysharp.com/reference/local/what-a-clean-check-means/ -->

## Summary        {#summary}

`osy validate` runs offline: no server, no database, no account. That is what makes it quick enough to run on every
save — and it means a few checks genuinely cannot run there, because they need state that only exists when the app is
compiled. A clean run lists them rather than implying it checked everything.

## Signature      {#signature}

```console
osy validate [path] [--json]
```

## Description    {#description}

A successful run ends with the checks that did **not** happen:

```console
✓ Validated successfully (4 entities, 7 functions)
  ⓘ 10 checks run only on a server compile:
    • app.Config settings resolved against .env.development / .env.production
    • the app.Config plaintext-secret warning
    • app.Audit surfaces resolved against the audit baseline entities
    • agent `Skills` / runbook `Uses` against the committed skill catalog
    …and 6 more — osy validate --json lists them all.
```

Each line names a **kind of check**, not a count, because a count is not something you can act on. The point is to
tell you *when* a local green is not the whole answer: if you have just edited `app.Config`, added a `control`, or
wired an agent to a skill, that is the moment to run a real compile.

Everything else `osy validate` reports is held to exact agreement with the server — the same rule, from the same
code, in the same words. The list above is the residue, and it is deliberately small.

### Why these need a compile   {#why}

They all read something that does not exist until the app is built:

- **`.env` values** — a setting with no `Default` takes its value from `.env.development` / `.env.production`, and an
  `.env` key naming no declared setting is an error. Neither file is in scope for a source check. See
  [per-environment config (app.Config)](https://osysharp.com/reference/config/app-config/).
- **Committed rows** — an agent's `Skills`, a runbook's `Uses`, and `app.Audit` surfaces resolve against rows the
  compile writes. A skill catalogue holds skills your source does not declare, so checking locally would report an
  unknown skill for one that exists.
- **`osyrin.lock`** — whether a `control` has a package pinned, and whether each `chunks { }` entry has a file. An
  unpinned control fails in the browser and nowhere else.
- **Metadata the platform owns** — `[Id(...)]` directives, `[Test(...)]` fixture links, counter scopes shared across
  entities.

## Examples       {#examples}

```console
osy validate                 # the whole project, with the footer
osy validate --json          # every skipped check, with a stable id and a reason
```

For tooling, the JSON carries a `notCheckedHere` array — **on failing runs too**, because a run that is about to go
green means no more than the one that just did:

```console
osy validate --json | jq '.notCheckedHere[] | .id'
```

## See also       {#see-also}

[The inner loop](https://osysharp.com/reference/local/the-inner-loop/) — where `osy validate` sits in the edit → check → run loop.

[Compiling your app](https://osysharp.com/reference/local/compiling-your-app/) — the compile that performs the checks listed above.

[Checking your app](https://osysharp.com/reference/local/checking-your-app/) — `osy lint`, the maturity signal, beside these correctness ones.

[per-environment config (app.Config)](https://osysharp.com/reference/config/app-config/) — `app.Config` and how a setting takes its value from `.env`.


---

<!-- https://osysharp.com/reference/local/why-a-run-is-not-moving/ -->

# Why a workflow run is not moving

> Opens one parked workflow run and says why it is sitting there — which state, which waits are outstanding, who is allowed to act on each of them right now, what would happen next, and when a deadline fires. Then steps it: raise the event it is waiting for and watch the transition.

<!-- id: local-why-a-run-is-not-moving · area: local · stability: stable · html: https://osysharp.com/reference/local/why-a-run-is-not-moving/ -->

## Summary        {#summary}

Answers the question somebody actually asks about a workflow: **this approval has been sitting there for three days —
why?** The run is opened by its own id or by the id of the thing it tracks, and the answer names the waits that are
outstanding and, for each one, the people the workflow's own rules would let act. Then `--raise` moves it.

## Signature      {#signature}

```console
osy workflow-runs                 # the fleet: which workflows, on which version, parked where
osy workflow-runs --runs          # one row per RUN, each with its id
osy workflow-run <id>             # why THAT one is not moving
osy workflow-run <id> --raise <Event> [--slot <Alias>] [--arg N=V] [--as <login>]
```

## Description    {#description}

### Two words that are not the same thing

A **flow** is a durable body suspended mid-execution — an awaited child run, a saga leg, a park point. It has a
**stack**, and a stack is what a debugger steps. That is what [Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/)'s sibling verbs
`osy debug-flows` and `osy debug-attach` address.

A **run** is a workflow state machine. One waiting on a `subscribe` slot or on a clock has **no stack at all** — it is
a row holding a position in a graph, and the only thing that moves it is an **event**. So a debugger cannot attach to
it, and asking for one is asking for frames that do not exist.

The two meet at exactly one point: a run whose *own body* parked mid-execution does own a continuation, and when it
does, this command prints the `osy debug-attach` line for it.

### What the read tells you

Give it a run and it answers in one call:

- **Where it is** — the state, how long it has been there, and the version its definition resolves through. If the
  state is one your current source no longer declares, it says so: that run is executing a definition you can no
  longer read, and `migrate` will not report it because it is already on the current version.
- **What it tracks** — the row, by its readable name, and the value of the property the workflow tracks.
- **Every outstanding wait** — the slot's name, the event it accepts *and that event's parameters*, whether it is
  unassigned or held, and by whom.
- **Who may act** — and this is the field the whole read exists for. See below.
- **What happens next** — one row per move, each naming the wait it fills, with its guard evaluated against this run
  right now. A move whose guard depends on the event's own payload stays offered and is marked as needing input:
  raising it is exactly what supplies the missing value.
- **What is blocking it** — any `Requires` criterion that is not met.
- **Every clock** — the SLAs, the state's `Expire`, the whole-run `Deadline`, and the reminders, each with when it
  fires. A deadline already passed reads as overdue, not as a negative interval.

### "Nobody has picked it up" and "nobody CAN" are different bugs

Every other screen shows an unclaimed wait the same way whoever is looking at it. But an unassigned slot has two
completely different causes:

- a person has not got round to it — someone will;
- the slot's `Candidates` rule admits **nobody** — a lead went on leave, a department was renamed, the only reviewer
  who matched is also the requester. Nobody can ever claim it, and the run will sit until a deadline fires.

So the candidate set is **evaluated now**, through the same rule every claim and deposit is checked against, and an
empty one is stated in words rather than printed as a zero. Where the pool is large the list is a sample and says so.

### Stepping it

`--raise <Event>` raises the event the run is waiting for and prints where it landed.

It runs as a **real principal**: `--as <login>` with that person's own password, exactly like
[Running a function](https://osysharp.com/reference/local/running-a-function/). The workflow's own rules then apply unchanged — an event declaring `[Authorize]` and a
slot declaring `Candidates` refuse a caller who does not satisfy them, and a raise with no principal named is
anonymous, which such a workflow correctly refuses. Holding developer authority over an app admits the *call*; it
never decides what the app's own rules allow.

`--slot <Alias>` names the wait being filled. It is needed whenever two slots subscribe one event — the ordinary
four-eyes shape, where `Approve` sits on both a legal and a finance wait — because the event name alone does not say
which one you are answering. The read prints the exact command to copy, with the slot already in it.

### Getting an id

`osy workflow-runs` groups runs, which is what makes it readable as a fleet view and is also why it carries no ids.
`--runs` lists them individually. You rarely need it: the id of the **tracked row** works too, and that is the one
already in front of you — in a URL, in a page, in the row you were just looking at. Either id may be given by its
first few characters, which is what the listing prints.

## Examples       {#examples}

An approval that has not moved. The run is opened by the purchase order's own id:

```console
$ osy workflow-run a6428528

PoApproval · Review (waiting)   run d77d3242-3b95-4cf4-8b1f-88d1eb42eb24
  PurchaseOrder "Rack of servers" · Status = Review
  in this state 21m · started 22m ago · version 1.0.0

WAITING ON
╭─────────┬─────────┬────────────┬───────────────────────────┬───────────╮
│ slot    │ event   │ status     │ who can act               │ breaches  │
├─────────┼─────────┼────────────┼───────────────────────────┼───────────┤
│ Finance │ Approve │ assigned   │ otto@acme.test (assigned) │ in 3h 38m │
│ Legal   │ Approve │ unassigned │ 3 candidates              │ in 3h 38m │
╰─────────┴─────────┴────────────┴───────────────────────────┴───────────╯
  Legal candidates: liam@acme.test, lena@acme.test, lars@acme.test

WHAT HAPPENS NEXT
  Approve → Rejected (fills Legal)
  Approve → Rejected (fills Finance)
  Cancel → Cancelled

CLOCKS
  reminder on Legal — fires in 1h 38m
  Assigned SLA on Legal — fires in 3h 38m
  run Deadline (whole run) — fires in 29d 23h

Step it:  osy workflow-run d77d3242-… --raise Approve --slot Legal --arg decision=<Decision> --arg reason=<string> --as <login>
```

Nobody is holding the legal wait, and three people could. That is a person, not a rule. Had the pool been empty it
would say so instead, in exactly the place the three names are.

Stepping it as one of them:

```console
$ osy workflow-run d77d3242 --raise Approve --slot Legal \
    --arg decision=Approve --arg reason="within budget" --as lena@acme.test

✓ Raised Approve on PoApproval as lena@acme.test.
  now in Review, waiting on Finance
```

The legal wait is filled and the finance one is not, so the run stays in `Review` — which is the workflow behaving
correctly, and the thing you were checking.

Raising a gated event with nobody named is refused, and the refusal says which rule refused it:

```console
$ osy workflow-run 27b71e0c --raise Cancel

✗ event 'Cancel' requires an authorized principal to raise it.
  This raise was ANONYMOUS. An event with `[Authorize]`, or a slot with `Candidates`, refuses that by design —
  name a principal with `--as <login>`.
```

Finding a run when you have no id at all:

```console
$ osy workflow-runs --runs

╭──────────┬────────────┬────────┬─────────────────┬────────────────┬──────────╮
│ run      │ workflow   │ state  │ item            │ waiting on     │ in state │
├──────────┼────────────┼────────┼─────────────────┼────────────────┼──────────┤
│ 27b71e0c │ PoApproval │ Draft  │ Standing desks  │ Submit         │ 16m      │
│ 33fd08f6 │ PoApproval │ Draft  │ Legal retainer  │ Submit         │ 16m      │
│ d77d3242 │ PoApproval │ Review │ Rack of servers │ Finance, Legal │ 15m      │
╰──────────┴────────────┴────────┴─────────────────┴────────────────┴──────────╯
```

## See also       {#see-also}

- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — the rule that decides who may act on a wait
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — declaring the wait itself
- [Transitions — where this item may go next](https://osysharp.com/reference/workflow/transitions/) — the same "what happens next" read, from inside the app
- [Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/) — what a run DID, when the question is a fault rather than a wait
- [Running a function](https://osysharp.com/reference/local/running-a-function/) — the other verb that acts as one of the app's own users


---

<!-- https://osysharp.com/reference/memory/how-retrieval-works/ -->

# How retrieval works

> What happens between `Memory.Search("…")` and the list you get back — indexing, matching, ranking and the two narrowings — written out as one pipeline. You never call any of it; this is here so the behaviour is explainable rather than mysterious, and so you can tell a bad result from a wrong expectation.

<!-- id: memory-how-retrieval-works · area: memory · stability: stable · html: https://osysharp.com/reference/memory/how-retrieval-works/ -->

## Summary        {#summary}
`Memory.Search("…")` is one call, and behind it are four decisions: **what got indexed**, **what the query is
matched against**, **how candidates are ordered**, and **what is returned**. None of it is yours to configure —
the settings are platform-wide and chosen by measurement, not by an app — but all of it is yours to *understand*,
because the difference between "retrieval is bad" and "I asked for the wrong thing" is usually visible from here.

The short version: **a `[Searchable]` field is cut into small pieces, the small piece is what gets matched, and
the whole field is what comes back.**

## Signature      {#signature}
The only surface is the one you already have. Everything below describes what it does.

```osy syntax
// You write this…
List<SearchHit> hits = Memory.Search("when do they want the report?", about: [customer], limit: 5);

// …and the pipeline decides the rest. There is no ranking knob on the call, deliberately: what a good
// ranking is turns out to be a question about the ENGINE, measured once, not a question an app can answer.
```

## Description    {#description}

### 1 · What gets indexed — small pieces, not whole fields   {#indexing}

A `[Searchable]` field is split into short pieces when it is saved. Each piece is indexed on its own, and the
**whole field** is kept alongside them as the thing an answer resolves to.

```osy syntax
entity Customer {
  [MaxLength(120)] string Name;
  [Searchable(Memory)] string Notes;   // ← split into short pieces on save; the whole text kept as the answer
}
```

The reason is that one column was doing two jobs that pull in opposite directions. **To be FOUND, text must be
short and specific.** A long note is stored as the average of everything it says, so a question about one sentence
inside it lands nowhere near the middle. **To be USEFUL, text must be long and complete** — a matched fragment on
its own is a fact with its context cut off.

Splitting the two is what lets both be true: **match the small piece, answer with the whole thing.** It is also
why you never see a fragment in a `SearchHit` — the piece that matched is an index entry, not a result.

⚠ **This is not a size setting you should reach for.** Pieces are small — around a sentence — and there is
deliberately no overlap between them, because the whole field is always one step away and repeating text between
neighbours only makes neighbouring pieces harder to tell apart.

### 2 · What the query is matched against   {#matching}

Two things run, and the second usually stays quiet.

**Meaning.** Your query is turned into a vector and compared against the indexed pieces. This is what makes
*"when do they want the report?"* find *"the monthly numbers go out as a spreadsheet"* — no words in common.

**Exact tokens.** If your query contains something a vector cannot represent — an order number, a product code,
an SKU — that token is *also* matched literally, and weighted heavily. If it contains no such token, this half
does not run at all.

```osy syntax
Memory.Search("did we ever sort out the packaging fault on PROD-4471?");
// → meaning finds "packaging fault … outer carton"
// → and `PROD-4471` is matched exactly, because a vector cannot represent an opaque code

Memory.Search("who do we chase about an unpaid invoice?");
// → meaning only. There is no distinctive token here, so the exact-match half stays silent.
```

⚑ **The silence is the feature.** Matching words is only useful when the words are distinctive. On an ordinary
question — where the wording is by definition *not* the answer's wording — a literal matcher returns confident
nonsense, and mixing confident nonsense into a good signal makes it worse. So it fires on the tokens it is for and
abstains on everything else.

You can still supply your own term with `keyword:` when you know one. That is trusted less than an extracted
token, not more: you chose a word, the extractor recognised a shape.

### 3 · How candidates are ordered   {#ranking}

Closeness comes first, then two adjustments.

**Age.** Between two pieces that match equally well, the more recent one wins. This is what makes a corrected fact
beat the thing it corrected — nothing else can separate them, since they are about the same subject and phrased
alike. The adjustment is gentle and it saturates: last week against last year is a real difference, ten years
against eleven is not.

```osy title="age breaks a tie — the correction beats what it corrected" syntax
// Remembered in March:     "invoices are settled by bank transfer"
// Remembered in November:  "they moved to card payments in the autumn"
Memory.Search("how do they pay?");   // → the November memory, though both match equally well
```

⚠ **Age is a tie-breaker, not a sort.** A much better older match still beats a barely-relevant newer one. If you
want strict recency you are asking for an ordinary query with an `OrderBy`, not for search.

**Being linked.** A memory reached by following a stated `Link` (see `related:`) is included, but no single linked
record may flood the result — it contributes its best piece, not its whole file. Being *related* is a reason to be
**considered**; it is not a reason to be **believed**.

⚠ **And the top answer belongs to a record you NAMED.** A linked memory can take every position below the first,
and it will when it matches better — but the lead result is about something in your `about:` list whenever there is
one to give. This is what keeps `related:` from changing the answer to a question you asked about one record:
widening the search should add what else is worth reading, not replace what you asked for.

```osy title="a linked memory never takes the lead from a record you named" syntax
// Even if the sister company's note is a WORD-FOR-WORD match and this customer's is only close,
// the customer's own memory leads — and the sister company's follows it, marked with the link that reached it.
Memory.Search("who signs off an urgent credit?", about: [customer], related: 1);
```

### 4 · What is returned, and what is filtered   {#returning}

You get whole answers, deduplicated: one `SearchHit` per underlying memory however many of its pieces matched, and
the text is the complete field rather than the fragment that won.

Two narrowings apply, and they are not the same kind of thing:

```osy syntax
// YOURS — a narrowing you asked for.
Memory.Search("payment terms", about: [customer]);        // only memory about this record
Memory.Search("payment terms", of: [Invoice], limit: 5);  // only these types, only five answers

// NOT YOURS — and not visible in the call at all.
// Every result is already restricted to memory anchored to rows you may read. A memory is reachable
// only through the record it is about, so if you cannot read the record you cannot reach its memory.
```

⚑ **Search is not a way around your app's security, and there is nothing to remember about that.** It is not a
filter applied to results afterwards — memory is unreachable in the first place unless the record it belongs to is
readable by the caller. That holds for every path, including a link hop, which checks both ends before it travels.

## Examples       {#examples}

The pipeline in one place, as it applies to a single call:

```osy title="the whole pipeline, on one call" test app=memory
using Osysharp.Memory;

entity Customer {
  [MaxLength(120)] string Name;
  [Searchable(Memory)] string Notes;    // 1. split into short pieces on save; whole text kept as the answer
}

List<SearchHit> WhatDoTheyWant(Customer customer) {
  // 2. matched on MEANING — this query shares no words with "the monthly numbers go out as a spreadsheet",
  //    and the exact-token half stays silent because there is no code or order number to match literally
  // 3. ordered by closeness, then nudged by recency, with a cap on what any one linked record contributes
  // 4. returned whole and deduplicated — one hit per memory, full text, never the fragment that matched;
  //    `about:` is the narrowing you asked for, and readability is the one you did not have to
  return Memory.Search("how should we send them their month-end numbers?", about: [customer], limit: 5);
}
```

⚠ **What the numbers on a `SearchHit` mean.** `Distance` is how far the matched piece was from your query — lower
is closer — and it carries the adjustments above, so it is a ranking figure and not a pure similarity. `Score` is
relevance, higher first. Neither is a probability and neither is comparable between two different queries.

## See also       {#see-also}
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — the call itself, and every argument it takes
- [[Searchable]](https://osysharp.com/reference/memory/searchable/) — marking a field as searchable, and what that costs
- [SearchHit](https://osysharp.com/reference/memory/searchhit/) — the shape of a result
- [Memory.Link / Memory.Unlink](https://osysharp.com/reference/memory/link/) — stating a relationship, and what `related:` walks
- [Search](https://osysharp.com/reference/memory/index/) — choosing between searching one entity and searching the app


---

<!-- https://osysharp.com/reference/memory/link/ -->

# Memory.Link / Memory.Unlink

> State that two records are related, in your app's own words — "supersedes, because it was renegotiated after the Q2 review". Every search hit about either record then carries the link, its words and the other record's name, so whoever reads the result can decide whether to go and fetch it.

<!-- id: memory-link · area: memory · stability: stable · html: https://osysharp.com/reference/memory/link/ -->

## Summary        {#summary}
`Memory.Link(a, b, …)` records that two records are related and **why**, in a sentence you write. Search then
carries that sentence on every hit about either record (`SearchHit.Links`), and `related:` can follow the link to
widen a search when you ask it to. `Memory.Unlink(a, b)` withdraws the claim. Both are available under
`using Osysharp.Memory;`.

## Signature      {#signature}
```osy syntax
using Osysharp.Memory;

bool Memory.Link(
    Entity a, Entity b,                  // the two records to relate
    LinkKind as = LinkKind.CrossReference,  // Citation | CrossReference | DerivedFrom | Bookmark | Supersedes
    string label = null,                 // short, read a → b: "superseded by"
    string reason = null,                // why, from a's side: "renegotiated after the Q2 review"
    string reverseLabel = null,          // short, read b → a: "supersedes"
    string reverseReason = null)         // why, from b's side: "replaces the Q1 deal at better terms"

int Memory.Unlink(Entity a, Entity b)    // how many links were removed
```

## Description    {#description}
A link is a **claim your app makes about two of its records**, not a foreign key. Use it when the relationship is
something someone decided rather than something the data structure implies: this deal supersedes that one, this
invoice cites that contract, this ticket was derived from that report.

### Both directions are stated, on purpose   {#both-directions}
A link is read from whichever end the reader arrived at, and **one sentence cannot be read backwards**: "supersedes"
from one end is "superseded by" from the other. So the call asks for both sides. If you state only the forward side,
the reverse falls back to it — right for a symmetric relationship (`CrossReference`), and noticeably odd for an
asymmetric one, which is the point: the surface asks rather than inventing a sentence for you.

### The words ride along with the record   {#annotation}
A link is **not** indexed as something to find on its own. Instead, every hit about either record carries it —
`SearchHit.Links` gives you the other record's type, id and name, the relationship read from *your* end, and your
sentence:

- a hit on the old deal carries *"superseded by — renegotiated after the Q2 review → Deal D-2026-02"*;
- a hit on the new one carries *"supersedes — replaces the Q1 deal at better terms → Deal D-2025-11"*;
- neither ever shows a reader the sentence written for the other end.

**Why not index the sentence itself?** Because it is about a PAIR and means nothing without both ends. On its own,
"renegotiated after the Q2 review" names nobody — it would match weakly when you searched for the record and
ambiguously when you did not. Attached to the record, it arrives in context, and whoever is reading decides whether
to fetch the other end. `related:` is still there for when you want the search itself to travel.

### Who stated it   {#author}
`Memory.Link` records the acting principal as the author, because a stated relationship is somebody's claim. It
**refuses an unauthenticated call**, naming the fix, rather than storing a claim nobody made. `Memory.Unlink` needs
no principal: withdrawing a claim is not itself a claim.

### What a reader is allowed to see   {#visibility}
A link's words describe a PAIR, so they are shown only to a reader who can read both records. That is checked for
you, on every path: a link whose other end you cannot read simply is not there, and a `related:` hop will not travel
through — or quote — a record you cannot see. Links are not a table your app queries; they are reached through these
two verbs and through the hits search returns, which is what makes that rule enforceable at all.

⚠ **A withheld link is not counted.** You are never told "3 links (2 hidden)" — that number would itself disclose
that two related records exist and that you are not cleared for them. A link you may not see is indistinguishable
from a link that was never stated. Where a record has *many* links, the list is capped for length and
`SearchHit.LinksElided` says how many the cap left out — that count is the cap's alone.

## Examples       {#examples}
```osy title="state a relationship, both ways" test app=memory
using Osysharp.Memory;

entity Deal { [MaxLength(120)] string Title; [Searchable(Memory)] string Notes; }

bool Supersede(Deal older, Deal newer) {
  return Memory.Link(older, newer,
    as: LinkKind.Supersedes,
    label: "superseded by",     reason: "renegotiated after the Q2 review",
    reverseLabel: "supersedes", reverseReason: "replaces the Q1 deal at better terms");
}
```

```osy title="withdraw it" test app=memory
using Osysharp.Memory;

int Withdraw(Deal a, Deal b) {
  // either order names the same link; the return says whether there was one to remove
  return Memory.Unlink(a, b);
}
```

## See also       {#see-also}
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — finding the words a link stated, and `related:` for following one
- [SearchHit](https://osysharp.com/reference/memory/searchhit/) — `Via`, the field a followed link fills in
- [Search](https://osysharp.com/reference/memory/index/) — how text becomes findable in the first place


---

<!-- https://osysharp.com/reference/memory/remember/ -->

# Memory.Remember / Memory.Forget

> Put a file's text into the app's searchable memory, filed against the record it is about — and take it back out again. Uploading a file does not do this; somebody has to say so, and the memory records who.

<!-- id: memory-remember · area: memory · stability: stable · html: https://osysharp.com/reference/memory/remember/ -->

## Summary        {#summary}
`Memory.Remember(file, about: record)` reads a stored file's text, splits it up, and adds it to the app's searchable
memory as memory **about** that record — so `Memory.Search` can answer from a document the way it answers from a
`[Searchable]` field. `Memory.Forget(file)` takes it back out. Both are available under `using Osysharp.Memory;`.

## Signature      {#signature}
```osy syntax
using Osysharp.Memory;
using Osysharp.Storage;

bool Memory.Remember(
    FileAsset file,       // the stored file whose text to index
    Entity about)         // the record this file is about — its memory is found through that record

int Memory.Forget(FileAsset file)   // how many memory entries were removed
```

## Description    {#description}
Storing a file and remembering it are two different acts, and this verb is the second one. An app can hold
attachments it never wants answered from — a signed copy, a scan kept for the record — and uploading is not a
decision about recall. So nothing is indexed until someone asks for it.

### `about:` is what makes it findable at all   {#about}
Memory is reached through the record it belongs to: you can find a file's text exactly when you can read the record
it was filed against. That is the whole of its access control, and it is why `about:` is required rather than
inferred. A file remembered about nothing would be memory nobody could ever read.

Filing the same file against a different record is a different claim, and you make it by calling the verb again with
that record.

### It records who asked   {#author}
Whoever calls `Memory.Remember` is the **author** of the memory it creates, so `Memory.Search(by: someone)` can
answer "what did they put in front of us", and `origin:` tells a document somebody filed apart from text that simply
followed the data. Because that authorship is the point, the call **refuses when nobody is authenticated** rather
than storing entries that claim to have appeared on their own.

### The work is queued, not awaited   {#queued}
Reading a document, splitting it and embedding it is real work, so `Memory.Remember` returns once the work is
**queued** — `true` when it was, `false` when this host has no worker to do it. The file becomes searchable shortly
afterwards. Remembering the same file twice is safe: the second call replaces that file's entries rather than
adding a second copy, which is also how a re-uploaded file stops answering with its old text.

### Forgetting   {#forget}
`Memory.Forget(file)` removes the file's entries from memory and returns how many there were, so "forgot it" and
"there was nothing to forget" are distinguishable without a second query. **The file itself is untouched** —
forgetting is about recall, not storage. A file you still hold is not a file you must still be answering from.

## Examples       {#examples}
```osy title="remember an attachment against the order it belongs to" test app=memory-remember
using Osysharp.Memory;
using Osysharp.Storage;

entity Order {
  [MaxLength(80)] string Reference;
  [Searchable(Memory)] string Notes;
}

bool Keep(Order o, FileAsset contract) {
  // from now on, searching this order can answer from the contract's text
  return Memory.Remember(contract, about: o);
}
```

```osy title="stop answering from it" test app=memory-remember
using Osysharp.Memory;
using Osysharp.Storage;

int Drop(FileAsset contract) {
  return Memory.Forget(contract);   // the file stays; only its memory goes
}
```

## See also       {#see-also}
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — asking questions of what you remembered, and `by:` / `origin:` for who filed it
- [[Searchable]](https://osysharp.com/reference/memory/searchable/) — the other way text gets into memory: a `[Searchable]` field, kept in sync for you
- [Memory.Link / Memory.Unlink](https://osysharp.com/reference/memory/link/) — stating why two records are related, in words search can find


---

<!-- https://osysharp.com/reference/memory/prune/ -->

# Memory.Statistics / Memory.Prune

> `Memory.Statistics` reports what your memory costs and what pruning at a given date would free — counted with the very rule the prune acts on, so it is the number you get. `Memory.Prune` then does it: drops the search VECTOR, keeps the text, never touches memory somebody chose to remember, and only ever runs because you called it.

<!-- id: memory-prune · area: memory · stability: stable · html: https://osysharp.com/reference/memory/prune/ -->

## Summary        {#summary}
A corpus grows. Every `[Searchable]` property, every remembered file, every indexed section carries a vector, and
vectors are the expensive part. `Memory.Prune` reclaims that space from the entries nothing has asked for.

## Signature      {#signature}
```osy syntax
MemoryStatistics Memory.Statistics(DateTime unusedSince)   // what it costs, and what pruning would free
int              Memory.Prune(DateTime unusedSince)        // → how many entries lost their vector
```

## Description    {#description}
Search records the day each memory was last RETURNED. `Memory.Prune` takes a date and acts on everything that has
not been returned since then.

**It drops the vector and keeps the text.** An entry without a vector stops being found by meaning — which is what
was costing storage — while remaining readable, still matched by exact words, and re-indexed the moment anything
changes it. Nothing is deleted.

**It never touches memory somebody chose to remember.** A `[Searchable]` property's text is still in the property,
so its index can always be rebuilt; a remembered file, a conversation turn, or a link's stated reason exists
nowhere else. Only the first kind is eligible, and that rule is the platform's rather than yours to pass.

**Nothing prunes on its own.** There is no background sweep and no retention setting that quietly forgets things —
a corpus that dropped what it had not been asked for lately is one you could not trust with a rare question, and
rare questions are what a memory is for. Somebody has to decide, which means somebody has to call this.

### Look before you prune   {#statistics}
`Memory.Statistics(unusedSince)` reports the same corpus from the same rule, so nobody has to prune to find out
what pruning does:

- **`StoredBytes`** — what your memory ACTUALLY occupies: rows, text, and every index over them, read from the
  database rather than worked out. It is usually much larger than `VectorBytes`, and the difference is real — a
  vector index is often as big again as the vectors it indexes, before the text is counted. `null` means the
  database could not say, which is "unknown", not "nothing".
- **`Entries` · `Vectors` · `VectorBytes`** — how much there is, and how much of that is vectors. `VectorBytes` is
  worked out from the count, so it is the *floor* of what the vectors cost, not the whole bill.
- **`Derived` · `Authored`** — what could ever be reclaimed, and what could not. `Authored` is the FLOOR: no
  cutoff, however aggressive, releases any of it.
- **`NeverUsed`** — entries no search has ever returned. The strongest signal that a corpus is carrying weight it
  does not need, with one honest caveat: a rare question nobody has asked yet looks exactly the same.
- **`Reclaimable` · `ReclaimableBytes`** — what a prune at *this* cutoff would free. Not an estimate: it is
  counted with the very filter `Memory.Prune` acts on, so it is the number you get.

Every figure is a count, so asking repeatedly is cheap — trying 30, 90 and 180 days to see the curve is the
intended use.

### Who may prune? Naming the memory operator      {#operator}
Both verbs are reserved for the app's **memory operator**, and you name that operator by declaring a policy:

```osy syntax
app.Memory = new MemoryConfig { Operator = IsOperator };
```

**This is not optional, and forgetting it is a compile error rather than a silent refusal.** An app that calls
`Memory.Statistics` or `Memory.Prune` without saying who may run them has not authorized anybody, so the build
stops and tells you what to write. The alternative would be a prune button that does nothing for the one person
it was built for, with no way to tell "nobody declared this" from "you are not an operator".

Say it once and you are done. The platform applies it at the verb, so the functions you write around it hold no
authority code — and neither does the second way in you add later, whether that is an agent's tool, an endpoint,
or another function calling the first.

## Examples       {#examples}
```osy title="who may manage this app's memory" test app=memory
using Osysharp.Memory;

[Role] enum AppRole { Member, Operator }

[Principal] entity Person {
  [Required, MaxLength(200), Unique] string Email;
  security { allow read when IsAuthenticated; }
}

entity RoleGrant {
  [Required] Person Grantee;
  [Required] AppRole Level;
  security { allow read when IsAuthenticated; }
}

// Anybody holding an Operator grant. It is an ordinary policy — a role, a flag on the person, membership of a
// team: whatever your app already means by "this is the person who looks after our data".
policy IsOperator => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Operator);

app.Memory = new MemoryConfig { Operator = IsOperator };
```

```osy title="show an admin what a cutoff would cost them" test app=memory
using Osysharp.Memory;

string MemoryReport(int olderThanDays) {
  var s = Memory.Statistics(DateTime.UtcNow.AddDays(-olderThanDays));
  return s.Entries + " entries using " + (s.StoredBytes / 1048576) + " MB, " + s.Vectors + " of them indexed. "
    + s.NeverUsed + " have never been returned. Pruning at " + olderThanDays + " days would free "
    + s.Reclaimable + " (" + (s.ReclaimableBytes / 1048576) + " MB). "
    + s.Authored + " were remembered on purpose and are never touched.";
}
```

```osy title="an admin action that reclaims space" test app=memory
using Osysharp.Memory;

// No authority check in here, and none is missing: `app.Memory.Operator` above already said who may, and the
// platform applies it at the verb rather than trusting each caller to remember.
int ReclaimMemorySpace() {
  // Anything derived that nothing has needed for six months gives up its vector.
  return Memory.Prune(DateTime.UtcNow.AddDays(-180));
}
```

```osy title="report before you act" test app=memory
using Osysharp.Memory;

string PruneAndReport(int olderThanDays) {
  var freed = Memory.Prune(DateTime.UtcNow.AddDays(-olderThanDays));
  if (freed == 0) { return "Nothing to reclaim — every memory has been used recently."; }
  return freed + " entries released their search index. Their text is unchanged.";
}
```

## See also       {#see-also}
- [[Searchable]](https://osysharp.com/reference/memory/searchable/) — what puts a property into the corpus in the first place
- [Memory.Remember / Memory.Forget](https://osysharp.com/reference/memory/remember/) — memory somebody chose to keep, which this never touches
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — the reads that record a memory as used


---

<!-- https://osysharp.com/reference/memory/index/ -->

# Search

> How you make text findable in Osy#. You never touch a vector, an index, or an embedder — you mark a text field [Searchable] and search it. The one decision is where you search: an entity's OWN rows with a ranking you compose, or one turnkey call across the whole app. Both give lexical relevance for free and upgrade to semantic automatically.

<!-- id: memory-index · area: memory · stability: stable · html: https://osysharp.com/reference/memory/index/ -->

## Summary        {#summary}
You make text findable by **marking a field `[Searchable]`** — and that is the whole setup. There is no index to
build, no embedding pipeline to run, and no vector that ever reaches your hands. You opt an app in with
`using Osysharp.Memory;`, mark the text you want to search, and then search it the way you already query data.

```osy title="mark a field, search it — no index, no vectors" test app=memory-index
using Osysharp.Memory;

entity Article {
  [MaxLength(200)] string Title;
  [Searchable] string Body;            // that is the entire setup

  security { allow create, read when IsAuthenticated || IsAnonymous; }
}

List<Article> Search(string q) {
  return Article
    .Where(a => a.Body.Matches(q))                  // narrow to candidates (index-backed)
    .OrderByDescending(a => a.Body.TextScore(q))    // rank them — YOUR formula
    .ToList();
}
```

Everything else in this area is two choices layered on top of that: **where** you search, and **which kind of
relevance** you get.

## Description    {#description}

### 1. The one decision — where you search   {#surfaces}
There are two search surfaces, and picking between them is the only real decision. You pick per field, with the
**scope** of [[Searchable]](https://osysharp.com/reference/memory/searchable/):

| You want to… | Surface | Scope | How you search |
|---|---|---|---|
| rank **one entity's own rows** and control the ranking yourself | **entity-local field search** | `[Searchable(Entity)]` | field primitives inside an ordinary query ([field search (Matches / TextScore / Similarity)](https://osysharp.com/reference/memory/field-search/)) |
| ask **one question across everything** and let the engine rank | **the shared corpus** | `[Searchable(Memory)]` | one call, `Memory.Search("…")` ([using Memory (semantic search)](https://osysharp.com/reference/memory/search/)) |

They are not two competing search engines — they are two ergonomics over the same relevance machinery:

- **Field search is a query with extra verbs.** `Prop.Matches(q)`, `Prop.TextScore(q)` and `Prop.Similarity(q)` are
  values you drop into a `Where` and an `OrderBy` — so you filter, order, threshold and weight them with plain
  arithmetic, exactly as you would any other query. You reach for it when you want *this entity's* rows and want to
  decide what "relevant" means. See [field search (Matches / TextScore / Similarity)](https://osysharp.com/reference/memory/field-search/).
- **The corpus is turnkey.** `Memory.Search("how do I get a refund")` embeds the query, searches a shared,
  cross-entity store, fuses lexical and semantic relevance for you, and returns a ranked `List<SearchHit>`
  ([SearchHit](https://osysharp.com/reference/memory/searchhit/)). You reach for it when you want *one answer over the whole app* and want the ranking done
  for you. See [using Memory (semantic search)](https://osysharp.com/reference/memory/search/).

If you write no scope, it is chosen by type — a `String` defaults to entity-local, a `Markdown` field defaults to
the corpus (Markdown is usually long and sectioned, so the corpus is its natural home). Write the scope explicitly
to override.

### 2. The relevance you get is the best available — for free   {#relevance}
Two kinds of relevance exist, and a `[Searchable]` field gives you both when it can:

- **Lexical (full-text)** — keyword matching. It needs no external service, so a `[Searchable]` field is useful the
  instant you deploy.
- **Semantic (vector)** — matching by *meaning*, so `"how do I get a refund"` finds a passage about *returns and
  money back* with no shared keywords. Semantic ranking is active whenever an embedding provider is configured.

The important part is that this is a **degrade, never a fail**: deploy with no embedder and your searchable fields
work as full-text and warn that semantic ranking is inactive; wire an embedder later and every `Full`-mode field
starts ranking by meaning too — **with no change to your code**. The **mode** of [[Searchable]](https://osysharp.com/reference/memory/searchable/) is where you
opt out of the semantic half (`TextOnly`) when keyword search is all you want and you would rather not carry the
per-row vector.

### 3. It is all queries and secured reads   {#security}
Nothing here is a new data path. Field search *is* a query — it obeys the same rules as [Querying data](https://osysharp.com/reference/query/index/), runs in the
database, and sees your uncommitted rows. `Memory.Search` is an ordinary read: a hit you are not allowed to read
never appears, exactly as a filtered query never returns a row you cannot see. There is no separate "search
permission" to configure and nothing extra to reason about — you declared who may read the entity, and search
returns what a read would.

### 4. Putting it together   {#together}
A field can serve one surface or the other, and an app can use both:

```osy title="both surfaces in one app" test app=memory-index
using Osysharp.Memory;

entity Doc {
  [MaxLength(200)] string Title;
  [Searchable(Memory)] string Summary;   // into the shared corpus — turnkey Memory.Search
  [Searchable(Entity)] string Notes;     // entity-local — rank Doc's own rows yourself

  security { allow create, read when IsAuthenticated || IsAnonymous; }
}

// turnkey: one ranked answer across the corpus
List<SearchHit> Ask(string q) {
  return Memory.Search(q, limit: 5);
}

// entity-local: this entity's rows, your ranking
List<Doc> ByNotes(string q) {
  return Doc.Where(d => d.Notes.Matches(q))
            .OrderByDescending(d => d.Notes.TextScore(q))
            .ToList();
}
```

## See also       {#see-also}
- [[Searchable]](https://osysharp.com/reference/memory/searchable/) — the `[Searchable]` attribute: **scope** (entity-local vs corpus) × **mode** (lexical vs +semantic)
- [field search (Matches / TextScore / Similarity)](https://osysharp.com/reference/memory/field-search/) — `Matches` / `TextScore` / `Similarity`: rank an entity's own rows with a ranking you compose
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — `Memory.Search`: one turnkey call over the shared corpus
- [SearchHit](https://osysharp.com/reference/memory/searchhit/) — the `SearchHit` result type `Memory.Search` returns
- [Memory.Link / Memory.Unlink](https://osysharp.com/reference/memory/link/) — `Memory.Link` / `Memory.Unlink`: state why two records are related, in words search can find
- [Memory.Remember / Memory.Forget](https://osysharp.com/reference/memory/remember/) — `Memory.Remember` / `Memory.Forget`: a stored file's text, in and out of the corpus
- [How retrieval works](https://osysharp.com/reference/memory/how-retrieval-works/) — what happens between the call and the list: indexing, matching, ranking, returning
- [Querying data](https://osysharp.com/reference/query/index/) — the querying model field search is built on


---

<!-- https://osysharp.com/reference/memory/searchhit/ -->

# SearchHit

> One result of a semantic search — the matched text, how relevant it was, where it came from, when it was learned, and what its record connects to. Enough to quote it and say where the quote is from. Returned by Memory.Search as a List<SearchHit>. Available under `using Memory;`.

<!-- id: memory-searchhit · area: memory · stability: stable · html: https://osysharp.com/reference/memory/searchhit/ -->

## Summary        {#summary}
`SearchHit` is a single result of a semantic search: the text that matched, a relevance `Score`, the id of the
entity the text came from, and the raw `Distance`. `Memory.Search` returns a `List<SearchHit>` ranked
most-relevant-first. It is available under `using Memory;` and cannot be redeclared by the app.

## Signature      {#signature}
```osy syntax
class SearchHit {
  // what matched
  string Content;          // the human-readable text that matched
  decimal Score;           // relevance — higher is more relevant
  decimal Distance;        // raw vector distance to the query — lower is closer
  MemoryKind Kind;         // a property chunk, a document section, a chat turn
  string? Via;             // the link's words, when `related:` reached this hit — null for a direct hit

  // which record
  Guid SourceEntityId;     // the entity instance this came from (a value, not a live reference)
  string SourceEntityType; // the NAME of the entity type that id belongs to
  string? SourceLabel;     // that record in words — its `semantic` render
  string? SourceDocument;  // the FILE the words are in, when it came from one

  // where, exactly
  string? Section;         // the heading this text sits under
  int? PageFrom;           // first page, when the source has pages
  int? PageTo;             // last page — "45-46" when it straddles a break
  int? SourceOffset;       // where this text starts in the source it was extracted from
  int? MatchOffset;        // where inside Content the match is
  int? MatchLength;        // how much of Content matched

  // when, and on whose say-so
  DateTime? RememberedAt;  // when this was learned
  MemoryOrigin Origin;     // Derived (the record restated) or Authored (somebody chose to remember it)
  Guid? AuthoredBy;        // who — null for Derived, always

  // what it connects to
  MemoryLink[] Links;      // what this record links to, and why
  int? LinksElided;        // how many links were left out for length
}
```

## Description    {#description}
A `SearchHit` is a plain in-memory value describing *why* a piece of content matched, so you can rank, filter, or
display results without re-fetching the source:

- **`Content`** — the text that matched the query.
- **`Score`** — relevance, higher is more relevant. When a `keyword` is supplied to the search, the score is the
  fused (hybrid) relevance; otherwise it is the semantic similarity.
- **`SourceEntityType`** — the NAME of the entity type `SourceEntityId` belongs to. You need both: the id alone
  says which row a hit came from but not which table, so it cannot be loaded, grouped or linked on its own.
- **`Kind`** — what kind of memory matched: a chunk of a `[Searchable]` property, a section of a document, a
  conversation turn. Worth reading when you build a prompt from hits — a recalled conversation turn and a stored
  field are different kinds of claim.
- **`SourceEntityId`** — the id of the entity the content came from. It is a plain `Guid` **value**, not a live
  entity reference — use it to load the full entity when you need it.
- **`Distance`** — the raw vector distance to the query embedding; lower is closer. `Score` is the value to rank
  on; `Distance` is exposed for tuning a `threshold`.
- **`Via`** — why this hit is here at all, when `Memory.Search`'s `related:` reached it by following a stated link
  instead of by being about one of the entities you named. It carries the link's words read in the direction you
  travelled, so a hit found through *your* deal reads "superseded by — renegotiated after the Q2 review" rather than
  the sentence the other end would read. Two hops read as a path (`"… → …"`), and a hit several links reached
  carries each of their reasons. It is **empty for a direct hit**, which is what lets you tell the two apart —
  and the hop is never folded into `Score`, because "similar to your query" and "linked to what you asked about"
  are different claims and one number cannot carry both.

### Saying where it came from   {#attribution}

A quoted fact nobody can check is not much better than an unquoted one, so a hit carries everything needed to
attribute it:

- **`SourceLabel`** — the record in words, rendered from its `semantic` template: *"Contract MSA-2024-11"*, beside
  the `SourceEntityId` a machine can act on. It is read fresh on every search, so renaming a record renames its
  citations immediately — nothing is re-indexed and nothing goes stale.
- **`SourceDocument`** — the file the words are in, when the memory came from one. You usually want this *and*
  `SourceLabel`: "Order SO-1001, p. 45" cannot be checked when the order has four attachments.
- **`Section`** — the heading the text sits under, when the source had headings.
- **`PageFrom` / `PageTo`** — the pages, when the source has pages. A range, because text straddles page breaks.
- **`SourceOffset`** — where the returned text begins in the source it was extracted from. The general form of the
  same question, for sources whose structure is not pages — a transcript's timeline, a source file's lines.
- **`MatchOffset` / `MatchLength`** — where inside `Content` the match actually is.
  `Content.Substring(MatchOffset, MatchLength)` is the sentence that matched, which is what you want to quote or
  highlight when `Content` is a whole page. Empty when the hit is short enough to be its own match.

### When, and on whose say-so   {#provenance}

- **`RememberedAt`** — when this was learned, not when a row was written. Two memories often disagree, and the
  later one is usually right; without a date you cannot tell which is later.
- **`Origin`** — `Derived` means the record restating itself (a `[Searchable]` property's text, a document
  section); `Authored` means somebody chose to remember it (a conversation turn, a file, a stated link). Worth
  reading before you present a hit as fact: *"the contract says"* and *"somebody said"* carry different weight.
- **`AuthoredBy`** — who, for authored memory. Null for `Derived` — asking who wrote a property's chunk is a
  category error, not a missing value.

### What it connects to   {#links}

**`Links`** carries what the hit's record is connected to, and why — each with the target's type, id and label, the
relationship read in the right direction, and the link's own stated words. It is an ANNOTATION, not a second
search: it describes what was already found, so it never changes the ranking and never crowds out a result. That is
the difference between it and `related:`, which genuinely widens the search and is opt-in for that reason.

A record with hundreds of links does not empty them into an answer — the list is capped, and **`LinksElided`** says
how many were left out. A link whose target you are not allowed to read is simply absent, and is never counted.

`SearchHit` is provided by the platform under `using Memory;`. Because `Memory.Search`'s return
contract depends on its exact shape, an app cannot declare its own type named `SearchHit` while the capability is
in use — doing so is a compile error.

## Examples       {#examples}
```osy title="rank and read hits" test app=memory
using Osysharp.Memory;

string BestMatch(string q) {
  var hits = Memory.Search(q, limit: 3);   // already ranked best-first
  if (hits.Count == 0) { return ""; }
  return hits[0].Content;
}
```

```osy title="quote a hit, and say where the quote is from" test app=memory
using Osysharp.Memory;

string Cite(string question) {
  var hits = Memory.Search(question, limit: 1);
  if (hits.Count == 0) { return "nothing on file"; }

  var hit = hits[0];
  var where = hit.SourceLabel;
  if (hit.Section != null) { where = where + " - " + hit.Section; }
  if (hit.PageFrom != null) { where = where + ", p. " + hit.PageFrom; }
  return hit.Content + " (" + where + ")";
}
```

```osy title="follow the thread the hit hands you" test app=memory
using Osysharp.Memory;

string SupersededBy(string question) {
  var hits = Memory.Search(question, limit: 1);
  if (hits.Count == 0) { return ""; }

  foreach (var link in hits[0].Links) {
    if (link.Label == "superseded by") { return link.TargetLabel + ": " + link.Reason; }
  }
  return "";
}
```

## See also       {#see-also}
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — the `Memory.Search` call that returns these hits
- [Memory.Link / Memory.Unlink](https://osysharp.com/reference/memory/link/) — how a link's words are stated, and why each end gets its own
- [How retrieval works](https://osysharp.com/reference/memory/how-retrieval-works/) — why a hit's text is a whole passage and the match is a range inside it


---

<!-- https://osysharp.com/reference/memory/searchable/ -->

# [Searchable]

> Mark a text field searchable. `[Searchable]` gives a String or Markdown property the best relevance search the app can offer — full-text always, semantic ranking when an embedder is configured. Scope picks entity-local field search vs the shared cross-entity corpus; mode picks lexical-only vs lexical+semantic.

<!-- id: memory-searchable · area: memory · stability: stable · html: https://osysharp.com/reference/memory/searchable/ -->

## Summary        {#summary}
`[Searchable]` marks a `String` or `Markdown` property as searchable. It gives the field the best relevance search
the app can offer: **full-text always works** (no setup), and **semantic ranking upgrades it** the moment an
embedding provider is configured — with no change to your code. Two optional arguments choose *where* the searchable
content lives (**scope**) and *which* kinds of relevance you get (**mode**). Requires `using Memory;`.

## Signature      {#signature}
```osy syntax
using Osysharp.Memory;

[Searchable]                      // scope defaults by type; Full
[Searchable(Entity)]              // entity-local field search; Full
[Searchable(Memory)]             // chunked into the shared corpus; Full
[Searchable(Entity, TextOnly)]   // entity-local; lexical only (no embedder needed)
```
Valid only on a `String` or `Markdown` property. Enums: `SearchScope { Entity, Memory }`,
`SearchMode { Full, TextOnly }`. Both arguments are optional and order-independent.

## Description    {#description}
A `[Searchable]` field participates in relevance search. What you get is governed by two orthogonal axes.

### Scope — where the searchable content lives   {#scope}
- **`Entity`** — the field is indexed **on its own row**, for *entity-local* field search. You query it with the
  field primitives `Prop.Matches(q)`, `Prop.TextScore(q)`, and `Prop.Similarity(q)` and compose your own ranking
  (see [field search (Matches / TextScore / Similarity)](https://osysharp.com/reference/memory/field-search/)). This is the default for a `String` property.
- **`Memory`** — the field's text is chunked into the app's **shared corpus**, the cross-entity store that
  [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) searches with one turnkey call. This is the default for a `Markdown` property (Markdown is
  typically long and sectioned, so the corpus is its natural home).

If you don't write a scope, it's chosen by the property's type: **`String` → `Entity`**, **`Markdown` → `Memory`**.
Write the scope explicitly to override — e.g. `[Searchable(Memory)] string Summary;` puts a short String field into
the corpus, and `[Searchable(Entity)] Markdown Body;` keeps a Markdown field's search entity-local.

### Mode — which kinds of relevance   {#mode}
- **`Full`** *(default)* — both **lexical** (full-text keyword match) and **semantic** (meaning-based, vector)
  ranking. Semantic ranking is active whenever an embedding provider is configured; without one, the field still
  works as full-text and upgrades automatically once an embedder is wired.
- **`TextOnly`** — **lexical only.** No embedder is ever needed, and the field carries no per-row vector. Use it
  when keyword search is all you want and you don't want the storage or the dependency. `TextOnly` applies to
  `Entity` scope only — the shared corpus is always hybrid, so `[Searchable(Memory, TextOnly)]` is rejected.

### Degrade, don't fail   {#degradation}
Full-text needs no external service, so a `[Searchable]` field is useful the instant you deploy. Semantic ranking
is an *upgrade*: declare an embedding for the app and every `Full` / `Memory` field starts ranking by meaning too —
no code change. If you deploy `Full` or `Memory` searchable fields with no embedder configured, the deploy succeeds
and warns that semantic ranking is inactive until you wire one; full-text is live in the meantime.

## Examples       {#examples}
```osy title="entity-local field search (default for String)" test app=memory
using Osysharp.Memory;

entity Article {
  [MaxLength(200)] string Title;
  [Searchable] string Body;            // Entity scope, Full mode — the String default
}
```

```osy title="lexical-only field (no embedder)" test app=memory
using Osysharp.Memory;

entity Note {
  [Searchable(Entity, TextOnly)] string Text;   // full-text keyword search only
}
```

```osy title="corpus fields for turnkey Memory.Search" test app=memory
using Osysharp.Memory;

entity Doc {
  [Searchable(Memory)] string Summary;   // a String explicitly placed in the corpus
  [Searchable] Markdown Body;            // Markdown defaults to the corpus
}
```

## See also       {#see-also}
- [field search (Matches / TextScore / Similarity)](https://osysharp.com/reference/memory/field-search/) — the field primitives (`Matches`/`TextScore`/`Similarity`) for `Entity`-scope fields
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — the turnkey corpus search over `Memory`-scope fields
- [SearchHit](https://osysharp.com/reference/memory/searchhit/) — the result type `Memory.Search` returns


---

<!-- https://osysharp.com/reference/memory/field-search/ -->

# field search (Matches / TextScore / Similarity)

> Rank an entity's own rows by a query on an [Searchable(Entity)] field. Matches is the keyword filter (bool), TextScore is the full-text relevance score, Similarity is the semantic (meaning) score. They return values you compare, order, and weight yourself — compose your own hybrid ranking with plain arithmetic.

<!-- id: memory-field-search · area: memory · stability: stable · html: https://osysharp.com/reference/memory/field-search/ -->

## Summary        {#summary}
Field search ranks an entity's **own rows** by a query against one of its `[Searchable(Entity)]` fields (see
[[Searchable]](https://osysharp.com/reference/memory/searchable/)). Three primitives give you the pieces, and you fuse them yourself:

- **`Prop.Matches(q)`** → `bool` — does the field match the keyword query? (the indexed candidate filter)
- **`Prop.TextScore(q)`** → `decimal` — full-text relevance (higher = better keyword match)
- **`Prop.Similarity(q)`** → `decimal` — semantic similarity (higher = closer in meaning)

Each returns a **value**, never a magic ranking. You compare, order, and weight them with ordinary expressions, so
*you* decide what "relevant enough" means — the compose-your-own-hybrid idiom below.

## Signature      {#signature}
```osy syntax
bool    Prop.Matches(string q)              // keyword predicate — use in Where
decimal Prop.TextScore(string q)            // full-text relevance score
decimal Prop.Similarity(string q)           // semantic score on a [Searchable(_, Full)] text field
decimal Prop.Similarity(Vector q)           // semantic score on a raw `Vector` field (bring your own vector)
```

## Description    {#description}
These primitives operate on a single entity's rows through an ordinary query — they behave exactly as any other
query does.

### They run in the query, and only there   {#query-only}

All three are database index operations: `Matches` and `TextScore` work against a full-text index built over the
field, and `Similarity` is a vector distance. None of them has an in-memory form, so they can be used **only inside a
query the database executes** — not on a row you have already loaded, not in a computed member, and not on the client.
Using one anywhere else is a compile error that says so.

```osy title="✗ these run in the query only, never on a row you hold" syntax
Note.Where(n => n.Body.Matches(term))   // ✓ the database answers it, using the index
loadedNote.Body.Matches(term)           // ✗ compile error — there is nothing to run it against here
```

If you need the answer on a row you are holding, ask the query for it: filter or order by these in the query that
loads the rows, and use what it returns.

### `Matches` — the indexed keyword filter   {#matches}
`Prop.Matches(q)` is a boolean full-text predicate: it's true when the field matches the keyword query `q`. It is
**index-backed**, so it's the efficient way to narrow a large table to the candidate rows before you rank them —
use it in `Where`.

### `TextScore` — full-text relevance   {#textscore}
`Prop.TextScore(q)` scores how well the field matches the keyword query (higher = better). Unlike `Matches`, a bare
score is **not** index-backed, so ranking by it alone would scan the whole table — filter with `Matches` first, then
order the survivors by `TextScore`.

### `Similarity` — semantic relevance   {#similarity}
`Prop.Similarity(q)` scores how close the field is to the query **in meaning** (higher = closer), so it finds
matches with no shared keywords. On a `[Searchable(_, Full)]` **text** field you pass a **string** and the engine
embeds it for you (once per query, never per row). On a raw `Vector` field you pass a **vector** you supply yourself.

`Similarity` needs a vector to compare against, so it's available only where one exists: a `Full`-mode searchable
field or a raw `Vector` field. On a `[Searchable(Entity, TextOnly)]` field (no vector) it's a compile error — use
`Matches`/`TextScore` there. Semantic ranking is active only when an embedding provider is configured; without one,
`Similarity` contributes nothing and your search degrades to full-text (see [[Searchable]](https://osysharp.com/reference/memory/searchable/)).

### Runtime thresholds   {#thresholds}
Because each primitive is a value, a relevance cutoff is just a comparison — the bound can be any expression (a
local, a parameter, a literal): `Where(c => c.Bio.Similarity(q) > minScore)`.

### Compose your own hybrid   {#hybrid}
The platform hands you the pieces; you fuse them with plain arithmetic and choose the weights. The idiomatic hybrid
filters with the indexed `Matches`, then orders by a weighted blend of semantic and lexical relevance:

```osy title="fuse the pieces yourself — indexed filter, then weighted rank" syntax
Candidate
  .Where(c => c.Bio.Matches(q))                                          // indexed candidate set
  .OrderByDescending(c => 0.7 * c.Bio.Similarity(q) + 0.3 * c.Bio.TextScore(q))
  .Take(k)
```

For a **turnkey** cross-entity search that fuses these for you over the shared corpus, use [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) instead;
reach for field search when you want entity-local results and control over the ranking.

## Examples       {#examples}
```osy title="keyword filter + full-text ranking" test app=memory
using Osysharp.Memory;

entity Article {
  [Searchable] string Body;
}

List<Article> Search(string q) {
  return Article
    .Where(a => a.Body.Matches(q))                     // indexed candidate filter
    .OrderByDescending(a => a.Body.TextScore(q))       // rank the survivors
    .ToList();
}
```

```osy title="semantic top-k with a runtime threshold" test app=memory
using Osysharp.Memory;

entity Candidate {
  [Searchable] string Bio;
}

List<Candidate> Best(string q, decimal minScore, int k) {
  return Candidate
    .Where(c => c.Bio.Similarity(q) > minScore)        // threshold is any expression
    .OrderByDescending(c => c.Bio.Similarity(q))
    .Take(k)
    .ToList();
}
```

```osy title="compose your own hybrid ranking" test app=memory
// Same `Candidate` as above — you decide how lexical and semantic scores are weighed.
List<Candidate> Hybrid(string q, int k) {
  return Candidate
    .Where(c => c.Bio.Matches(q))
    .OrderByDescending(c => 0.7 * c.Bio.Similarity(q) + 0.3 * c.Bio.TextScore(q))
    .Take(k)
    .ToList();
}
```

## See also       {#see-also}
- [[Searchable]](https://osysharp.com/reference/memory/searchable/) — the `[Searchable]` attribute that makes a field searchable (scope × mode)
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — turnkey hybrid search over the shared corpus (`Memory`-scope fields)
- [SearchHit](https://osysharp.com/reference/memory/searchhit/) — the result type `Memory.Search` returns


---

<!-- https://osysharp.com/reference/memory/search/ -->

# using Memory (semantic search)

> Opt into semantic (vector) search over your app's content. `using Memory;` adds a searchable store to the app; `Memory.Search("…")` returns the best-matching content as a ranked `List<SearchHit>`, embedding the query for you — no vectors in your hands.

<!-- id: memory-search · area: memory · stability: stable · html: https://osysharp.com/reference/memory/search/ -->

## Summary        {#summary}
`using Memory;` turns on semantic search for an app: it adds a searchable content store, and the
`Memory.Search("…")` call returns the content most relevant to a natural-language query as a ranked
`List<SearchHit>`. The query is embedded for you — you write text, never vectors — and results come back ordered
by relevance. Without the `using`, the store and `Memory.Search` don't exist in the app; opting in is what makes
them available.

## Signature      {#signature}
```osy syntax
using Osysharp.Memory;

List<SearchHit> Memory.Search(
    string query,                 // natural-language text (embedded for you)
    Entity[] about = null,        // null/empty = the whole corpus; else memory ABOUT these entities
    int limit = 20,               // maximum hits
    decimal threshold = 0.5,      // maximum distance (closer = smaller)
    string keyword = null,        // optional keyword for hybrid ranking
    Type[] of = null,             // restrict to these entity TYPES
    Entity[] by = null,           // restrict to what these principals AUTHORED
    MemoryKind[] kinds = null,    // restrict to these kinds of memory
    MemoryOrigin origin = null,   // Derived (follows the data) or Authored (somebody chose it)
    int related = 0)              // also search what `about` is LINKED to, this many hops out
```

## Description    {#description}
Semantic search finds content by **meaning**, not exact words: a search for `"how do I get a refund"` surfaces a
passage about *returns and money back* even with no shared keywords. You opt an app in with a single declaration:

```osy syntax
using Osysharp.Memory;
```

That brings three things into the app:

- **a searchable content store** — the app now holds a store of embeddable content. Opted-in apps may query it
  directly like any other data, but `Memory.Search` is the ergonomic default and the one you'll reach for.
- **`Memory.Search(...)`** — the search call itself, described below.
- **`SearchHit`** — the result type each hit comes back as (see [SearchHit](https://osysharp.com/reference/memory/searchhit/)).

None of this exists in an app that doesn't opt in — there is no store to write, nothing to search. The `using` is
the switch.

### What fills the corpus   {#corpus}
`Memory.Search` searches a shared, cross-entity **corpus** — you don't write to it directly. Two authoring surfaces
contribute content to it:

- **`[Searchable(Memory)]` fields** — a `String` or `Markdown` property marked for the corpus is chunked in and
  kept in sync as rows change (see [[Searchable]](https://osysharp.com/reference/memory/searchable/)). This is the usual way to make an entity's text findable.
- **the `semantic =>` entity card** — a composed template that embeds a rendered summary of a whole row into the
  corpus.
- **`Memory.Remember(file, about: row)`** — a stored file's text, filed against the record it is about. Unlike the
  two above it is not automatic: uploading a file indexes nothing until somebody asks for it ([Memory.Remember / Memory.Forget](https://osysharp.com/reference/memory/remember/)).

For entity-local search over a single field — ranking an entity's *own* rows and controlling the ranking yourself —
use the field primitives in [field search (Matches / TextScore / Similarity)](https://osysharp.com/reference/memory/field-search/) instead; `Memory.Search` is the turnkey call across the whole
corpus.

### The call   {#call}
`Memory.Search(query, …)` embeds `query`, ranks the stored content against it, and returns up to `limit` hits
best-first. Every argument past `query` is optional and tunes the search:

- **`query`** *(required)* — the natural-language text to match. You never construct or pass a vector; the engine
  embeds the text.
- **`about`** — zero or more entities the memory should be **about**. Leave it out (or pass `null`/an empty list)
  to search the whole accessible corpus; pass one or more entities to restrict hits to *their* content. The list may
  mix entity types. This narrows the search itself, not the result — so `limit` applies to the entities you named,
  and a hit about them is never crowded out by the rest of the corpus.
- **`limit`** — the maximum number of hits to return (default `20`).
- **`threshold`** — the maximum distance a hit may have; smaller is closer, so a lower threshold is stricter
  (default `0.5`).
- **`keyword`** — an optional keyword. When supplied, ranking fuses semantic similarity with a keyword match
  (hybrid search) so an exact term still pulls its content up. A row matching more of the keyword's words ranks
  higher; matching them all is not required.

  **Pass a distinctive term, not the query.** `keyword` is for the word you know appears verbatim — a part code, a
  clause name, `"warranty"`. Handing it the whole question makes retrieval *worse*, not better: a question is mostly
  ordinary words that appear everywhere, so the keyword half stops discriminating and dilutes the semantic half,
  which was already going to find the answer. If you have no particular term in mind, leave `keyword` out.
- **`of`** — zero or more entity **types**, when you want (say) only deals and contracts. `about` names instances;
  `of` names types, and the two combine.
- **`by`** — the principals whose memory to return. Only *authored* memory has an author — a conversation turn, a
  stated link, a file someone chose to remember — so this excludes derived content by construction.
- **`kinds`** — which kinds of memory to return: a chunk of a property, a section of a document, a conversation
  turn. Use it when a hit's kind changes what you would do with it.
- **`origin`** — `MemoryOrigin.Derived` (it follows the data — the chunks of a `[Searchable]` field, a document's
  sections) or `MemoryOrigin.Authored` (somebody chose it: a conversation turn, a stated link, a file passed to
  [Memory.Remember / Memory.Forget](https://osysharp.com/reference/memory/remember/)).
- **`related`** — how many stated links to follow OUT from `about`, so the search also covers what those entities
  are linked to (0, the default, follows none; at most 3). It needs `about` — following links out of "the whole
  corpus" would reach everything linked to anything, so asking for it without a starting point is a compile error.
  Each hit reached this way carries the link's words in [SearchHit](https://osysharp.com/reference/memory/searchhit/)'s `Via`, and a direct hit leaves `Via`
  empty — see [Memory.Link / Memory.Unlink](https://osysharp.com/reference/memory/link/) for how those words get stated in the first place.

A related hit is **marked, never re-ranked**. Its `Score` means exactly what every other hit's `Score` means; the
fact that a link led you to it is a different claim about relevance, and blending the two into one number would
leave that number meaning neither. So you can show "found because it is similar" and "found because your deal
supersedes it" as the different things they are.

Results are always **ranked** — the list comes back ordered by relevance, most-relevant first — and always
**secured**: a hit you aren't allowed to read never appears, exactly as with an ordinary query.

`Memory.Search` reads content and embeds text, so it is an **effect** (like a data read): its result is held
across a durable suspend/resume without re-running the search.

### The result   {#result}
Each hit is a `SearchHit` carrying the matched text and how relevant it was — `Content`, `Score`
(higher = more relevant), `SourceEntityId` and `SourceEntityType` (which row, and which type, the content came
from), `Kind` (what sort of memory matched), `Distance`, and `Via` (the link that led here, when `related` did).
See [SearchHit](https://osysharp.com/reference/memory/searchhit/) for the full shape.

## Examples       {#examples}
```osy title="search the whole corpus" test app=memory
using Osysharp.Memory;

List<SearchHit> FindArticles(string q) {
  return Memory.Search(q, limit: 5);
}
```

```osy title="scoped + hybrid search" test app=memory
using Osysharp.Memory;

entity Product { string Name; }

List<SearchHit> RelatedTo(Product p, string q) {
  // restrict hits to this product's content, and let an exact keyword pull matches up
  return Memory.Search(q, about: [p], limit: 10, threshold: 0.4, keyword: "warranty");
}
```

```osy title="follow what a deal is linked to" test app=memory
using Osysharp.Memory;

entity Deal { [MaxLength(120)] string Title; [Searchable(Memory)] string Notes; }

List<SearchHit> AroundThisDeal(Deal d, string q) {
  // this deal's own memory, plus memory about whatever it is linked to, one hop out.
  // Anything reached that way comes back with `Via` filled in; anything found directly does not.
  // The LEAD hit is still about this deal — a linked record widens what you get to read, it does not
  // take over the answer. See [How retrieval works](https://osysharp.com/reference/memory/how-retrieval-works/) for what that costs and what it buys.
  return Memory.Search(q, about: [d], related: 1, limit: 10);
}
```

## See also       {#see-also}
- [Memory.Remember / Memory.Forget](https://osysharp.com/reference/memory/remember/) — putting a file's text into the corpus, and taking it back out
- [Memory.Link / Memory.Unlink](https://osysharp.com/reference/memory/link/) — stating the links `related` follows, and what their words mean at each end
- [SearchHit](https://osysharp.com/reference/memory/searchhit/) — the `SearchHit` result type each hit comes back as
- [[Searchable]](https://osysharp.com/reference/memory/searchable/) — the `[Searchable]` attribute; `[Searchable(Memory)]` feeds this corpus
- [field search (Matches / TextScore / Similarity)](https://osysharp.com/reference/memory/field-search/) — entity-local field search (`Matches`/`TextScore`/`Similarity`) when you want to rank one entity's own rows
- [Dynamic IN (list.Contains in a query)](https://osysharp.com/reference/query/dynamic-in/) — filtering an ordinary query by a runtime set


---

<!-- https://osysharp.com/reference/project/app-versions/ -->

# Deploying while workflows are running

> A workflow run can outlive the deploy that started it. Deploying with --new-version freezes the code and data shape the running app has, so runs already in flight finish on what they started with while new runs use what you just deployed.

<!-- id: project-app-versions · area: project · stability: stable · html: https://osysharp.com/reference/project/app-versions/ -->

## Summary        {#summary}
A workflow run can last far longer than the gap between two deploys — an approval that waits three days, an onboarding
saga that waits for a signature, a fan-out that waits on a hundred children. So a deploy will land **while runs are
partway through**, and the question that decides whether your app is correct is: *when that run wakes up, whose code
does it execute?*

Deploy with `--new-version` and the answer is **its own**. The deploy is frozen as a new app version; every run already
in flight goes on resolving and executing against the code and the data shape it started under, and every run started
after the deploy uses what you just deployed. Both versions run side by side until the older runs drain.

## Signature      {#signature}
```osy syntax
osy compile --new-version           // local
osyrin app compile --new-version    // a deployed app
```

## Description    {#description}

### What a version is   {#what}
An app version is a frozen snapshot of what the app **is**: its entities, functions, workflows, screens, and the shape
of its data. Deploying with `--new-version` records one and makes it current. Nothing is copied and nothing is
duplicated — your rows stay exactly where they are, in one place, shared by every version. What is frozen is the
*description* of them.

That split is what makes the guarantee cheap. A run pinned to an older version reads and writes the same rows as
everything else; it just reads them through the shape it was compiled against.

### What a run in flight keeps   {#in-flight}
A run that was already going when you deployed keeps:

- **its logic** — the body of the workflow, and of every function that workflow calls, exactly as it was written when
  the run started. Editing what a step does will not change what a run halfway through that step is going to do;
- **its data shape** — the properties it knew about. A property you removed in the new version is still there for that
  run, and its column is retained until no run needs it any more.

A run started *after* the deploy gets the new logic and the new shape. Nothing you do to source affects a run that has
already begun.

### When you need the flag   {#when-needed}
- **Long-running workflows.** Anything that waits: an approval, a scheduled reminder, a saga waiting on a child, an
  external callback. The longer it waits, the more likely a deploy lands under it.
- **Any change to a workflow, or to a function a workflow calls.** Body edits count — changing what a step *does* is
  precisely the change a run in flight must not see.

You do not need it for a change nothing is waiting on. Deploying a new screen, a new report or an unrelated entity is
an ordinary deploy.

### When it happens without you asking   {#automatic}
A **production** app decides for itself: it freezes a version when the change would affect a run in flight, and
otherwise deploys in place. `--new-version` overrides that and always freezes one.

A **development** app never freezes a version unless you ask. That keeps the inner loop fast — you edit, compile, and
run against exactly what you last wrote. Pass `--new-version` when you specifically want to rehearse the upgrade: park
a run, deploy, and watch it finish on its old body.

### Seeing what versions you have   {#listing}

```console
$ osy versions            # local
$ osyrin app versions     # a deployed app
```

```text
NewAdmin — application versions
╭─────────┬────────┬───────────┬──────────────────────────┬────────────────────────╮
│ Version │ Schema │ In flight │ Oldest run               │ State                  │
├─────────┼────────┼───────────┼──────────────────────────┼────────────────────────┤
│ 1.0.0   │ app    │         2 │ 2026-07-25 16:47 (3h ago)│ held by runs in flight │
│ 1.1.0   │ app_v2 │         0 │ —                        │ current                │
╰─────────┴────────┴───────────┴──────────────────────────┴────────────────────────╯
```

**In flight** is the number that matters. It is why a version is still there, and it is the only thing standing between
an old version and being cleaned up. `Oldest run` tells you how long you have been waiting for the last stragglers to
finish. Add `--json` for a machine-readable form.

### Old versions clean themselves up   {#reclaiming}

You do not accumulate a version per deploy. Once **nothing is running against** a version any more, the next deploy
reclaims it, and `versions` shows it as `reclaimed`. Two versions are never touched:

- the **current** one, which new runs start on;
- **any version with a run still in flight** — dropping it would strand that run with nothing to resolve against, which
  is the exact failure versioning exists to prevent.

So the usual steady state is one version, plus however many are still finishing work. A long-waiting run is the normal
reason to see an older version hanging around, and the `In flight` column tells you which one.

Reclaiming a version also eventually reclaims the **columns of properties you removed**. Deleting a property does not
drop its column straight away: runs on older versions may still be reading and writing it. Once every version that
declared the property is gone, the column can go too — a separate, explicit step, because unlike a version schema a
column holds the only copy of its data. Only columns whose removal your app actually recorded are taken; anything else
that turns up on a table is reported to you and left alone.

Removing a property or an entity is itself a change you have to acknowledge — see
[Renaming and removing things that hold data](https://osysharp.com/reference/project/renaming-and-removing/).

### The one change that is refused   {#refused}
Changing the stored **type**, **width** or **precision** of a property that already exists is refused on an app with
data, whether or not you version it. There is no shape a column can take that is simultaneously the old type for a run
still pinned to it and the new type for the code you just deployed.

Evolve the type additively instead:

1. add a **new** property of the new type — an additive change deploys cleanly and disturbs nothing;
2. copy the data across;
3. once every reader **and writer** uses the new property, remove the old one — a removal like any other, so it needs
   its one line of acknowledgement (see [Renaming and removing things that hold data](https://osysharp.com/reference/project/renaming-and-removing/)).

> While both properties exist, a write to the old one is **not** reflected in the new one. Move all writes over before
> the final copy, or write to both during the transition — anything written to the old property in that window is lost.

Removing a **required** property in step 3 makes its retained data optional. It has to: the version you just deployed no
longer knows about the property, so it has nothing to put there, and rows created from now on simply leave it empty. A
run still pinned to an older version will therefore find that value missing on any row created after the removal, even
though the version it was compiled against believes the property is always present. Finish the cutover — step 2 — before
anything you care about starts depending on the old property being filled in.

### Deploying is still one command   {#deploy}
Everything else a deploy does is unchanged when it freezes a version: your icons, artwork, control packages and
per-environment configuration are part of the deploy and land with it, and the app's generated API description is
refreshed to match what you just shipped.

## Examples       {#examples}

A workflow whose run can easily outlive a deploy — it waits for a human:

```osy title="a run that will still be waiting when you deploy" test app=app-versions
enum ApprovalStage { Pending, Approved, Rejected }

entity Invoice {
  [Required, MaxLength(120)] string Description;
  [Required] decimal Amount;
  [MaxLength(200)] string? Outcome;
  ApprovalStage Stage = ApprovalStage.Pending;
}

workflow ExpenseApproval {
  Tracks = Invoice.Stage;
  Initial = Pending;

  event Decide();

  state Pending {
    subscribe Decide();
    on Decide {
      when (this.Item.Amount <= 500) {
        this.Item.Outcome = "auto-approved under the original policy";
        goto Approved;
      }
      default { goto Rejected; }
    }
  }

  terminal success Approved { }
  terminal success Rejected { }
}
```

Now raise the auto-approval threshold to `1000`, reword the outcome, and deploy:

```bash
osy compile --new-version
```

An invoice already sitting in `Pending` when that deploy landed is still judged at `500` when the decision finally
arrives, and still writes *"auto-approved under the original policy"* — it finishes under the policy it entered. An
invoice submitted after the deploy is judged at `1000`. Neither one had to know the other existed.

## See also       {#see-also}
- [Renaming and removing things that hold data](https://osysharp.com/reference/project/renaming-and-removing/) — renaming or removing something that already holds rows
- [Stopping runs after a bad deploy](https://osysharp.com/reference/project/cancel-runs/) — stopping runs that are still executing a version you want rid of
- [app.osy](https://osysharp.com/reference/project/manifest/) — what a deploy is built from
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — starting a child workflow and waiting for it
- [Workflow.BeginSaga (a compensating saga scope)](https://osysharp.com/reference/workflow/beginsaga/) — long-running sagas, the runs most likely to span a deploy


---

<!-- https://osysharp.com/reference/project/hosting/ -->

# Hosting an Osy# app

> You never set up a database. You never started a web server. That wasn't a step we skipped — it's the product.
> 
> Your app is two things and you only write one of them: you write the source, Osyrin runs it. Today it is hosted one way, on your own machine; self-hosting is next.

<!-- id: project-hosting · area: project · stability: stable · html: https://osysharp.com/reference/project/hosting/ -->

## Zero to running — local dev   {#zero-to-running}

```console
osy init --agent claude
osy launch
```

That's it. `osy launch` compiles your project and opens it, starting the platform for you if one isn't already up
— a real PostgreSQL (with pgvector), a real web server, and the entire runtime on your machine. No account. No
cloud project. No connection string. Nothing to sign up for.

This is not a simulator or a mock. It's the same runtime with the same enforcement, which is why what works on your
laptop is what works, full stop.

Make a change, compile, and it's on screen in seconds.

## Your app is two things. You only write one of them.   {#summary}

You write the source. [Osyrin](https://osysharp.com/reference/project/runtime/) runs it.

Everything this documentation calls "built in" — the database, the web server, the client, the security model
inside every query, durable workflows, the realtime channel — isn't something you wired up. It's already running.
Your source is one half of the app. Osyrin is the other half, and it's the bigger one — [what it promises while it runs](https://osysharp.com/reference/project/runtime/) is its own page.

Most teams spend the majority of their effort on that half. You spend it on the business.

## What you get without writing any of it   {#description}

**Your data.** The schema comes from your model and follows it when the model changes. Constraints enforced on
write. A function's writes land as one commit. Counters stay right under load. Destructive changes are refused
until there's a migration you can actually read.

**Who sees what.** The `security { }` you declared is compiled into the query itself — a row filter and a field
mask under every read your program can express. Not a check you remembered to add. Not a middleware someone forgot.

**The screen.** Routing, rendering, reactivity, a query that stays live and pushes updates, forms, the control kit,
light and dark themes, and the hand-off to the server when a click needs one. Reconnects itself when the network
drops.

**Work that outlives the request.** Workflow states, SLA clocks, reminders that fire on wall time. Effects that run
exactly once — across a crash, across a deploy. Running instances keep executing their own version while you ship
the next one.

**The outside world.** Typed HTTP clients. A REST API over your model. An MCP server for agents. Scheduled work.
File storage.

**Intelligence.** LLM providers with streaming, tool calls and budgets. Embeddings and vector search from a
`[Searchable]` attribute.

**Seeing what happened.** One correlation id from click to commit. Traces you can open. Faults captured whether or
not you were looking.

Every line of that is a thing a team normally builds, operates, and keeps compatible with everything else. Here,
it's simply there.

## Deploying is not a binary swap   {#not-a-binary-swap}

Here's the thing that catches people: a compile doesn't produce a binary, a container, or a bundle. It produces
your app's *model* and applies it to a platform that is already running.

So a deploy is a new version of a description landing on a live platform. No process gets replaced. Nothing gets
drained. Workflows that were mid-flight keep going on the version they started under, and new runs pick up the one
you just shipped.

You've never had a deploy that worked like that. It's what happens when the runtime owns the runtime.

## Things you will never do   {#never}

- Size a server
- Pick a database tier
- Configure a connection pool
- Buy a domain, provision TLS, open a firewall port
- Stand up a load balancer
- Write a backup schedule, then rehearse the restore
- Tune autoscaling
- Add a migration runner to CI
- Push to a container registry

That list is the gap between "I have an app" and "other people can use it." On most stacks it's a week of decisions
before the first person outside the room sees anything. Here it's the same command that's been running on your
machine all along.

## Once you're happy with it locally — self-hosted, and cloud   {#deploying}

The same source goes to a platform that isn't your laptop, with the verbs you have already been using — you point
them at a server instead of letting one start itself:

```console
osyrin login --server <url>                       # once
osyrin app create "Reelo" --server <url>          # once
osyrin app compile --server <url> --new-version   # every deploy, frozen as a version
```

`--new-version` is what makes a deploy safe while people are using the app: a run already in flight finishes on the
code and data shape it started under, while new runs pick up what you just shipped.

### The free tier, and what happens to an app nobody uses   {#free-tier}

A free hosted app is meant to be run, not just parked: it comes with enough execution time to serve a small app for
real, and more usage is what the paid tiers buy — more of that time, not a different platform. A free hosted app
runs for as long as someone uses it. An app that nobody has visited, deployed to or signed in to
for ninety days is marked dormant and its owner is told; any visit, deploy or sign-in resets the clock. Thirty days
after that notice, still untouched, the app and its data are removed — after a final backup, which stays
downloadable for thirty more days. Paid apps are never removed for inactivity, and none of this applies to the
local runtime or to a server you run yourself, which are your machines. The exact numbers are the tier's terms and
may be shortened with thirty days' notice on this page.

## Where it runs today   {#today}

Straight talk: this is early, and one hosting shape ships right now.

| | |
|---|---|
| **Developer hosting** | **Available now.** `osyrin dev` on your own machine. Real runtime, real database, one command. Everything in this reference runs on it. |
| **Self-hosting** | **Next.** Run Osyrin on infrastructure you control. |

The path is already built into the command you use every day. `osyrin app compile --server <url>` doesn't care
whether that platform is your laptop or a box in your own cloud — the model is the artifact, and the artifact
doesn't know where it's being applied. Everything you build against the local platform carries over unchanged.

## Try it   {#try-it}

Install the CLI, run `osy init --agent claude`, run `osy launch`. You'll have an app with a database, auth, and a
live UI before you've finished your coffee — and you'll have written none of the parts that usually take the week.

## See also   {#see-also}
- [Osyrin, the runtime](https://osysharp.com/reference/project/runtime/) — Osyrin itself: what it promises while it runs, and what stands behind it
- [Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/) — running a local platform
- [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/) — deploying while workflows are running
- [Renaming and removing things that hold data](https://osysharp.com/reference/project/renaming-and-removing/) — renaming and removing things that hold data
- [How an Osy# app works (the execution model)](https://osysharp.com/reference/project/index/) — how an Osy# app works


---

<!-- https://osysharp.com/reference/project/index/ -->

# How an Osy# app works (the execution model)

> Read this before you write anything. An Osy# app is ONE model — you do not build an API for it, and you do not write the layers you are probably expecting: no controllers, no routes, no DTOs, no serializers, no fetch layer, no client store, no ORM, no auth middleware. A page reads data by naming the entity; a button calls a server function by name. What is left for you to write is the business problem. What you must still reason about is small, and this page says exactly what it is.

<!-- id: project-index · area: project · stability: stable · html: https://osysharp.com/reference/project/index/ -->

## Summary        {#summary}
**You do not write an API for your own app.**

A page reads data by **naming the entity**. A button calls a server function **by name**. The engine carries the flow
across the client/server boundary for you — durably — and your security rules are already inside the query when it
runs.

```osy title="a whole feature: the data, the rule, the server, the screen" test app=project-index
[Role] enum AppRole { Authenticator, Member }

[Principal]
entity User {
  [Required, MaxLength(255)] string Email;
  [MaxLength(200)] string PasswordHash;
  security {
    allow read where Id == user.Id;                   // the rule lives HERE — not in an endpoint
    deny read PasswordHash when !IsAuthenticator;     // …and the credential is masked from everyone but sign-in
  }
}

policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);

entity RoleGrant {
  [Required] User User;
  [Required] AppRole Role = AppRole.Member;
  security { allow read where User == user; }
}

entity Note {
  [Required, MaxLength(200)] string Title;
  security {
    allow read, update, delete where CreatedBy == user.Id;   // …and it is compiled INTO every query
    allow create when IsAuthenticated;
  }
}

// An ordinary server function. No route, no controller, no request/response type.
Note Publish(string title) {
  return new Note { Title = title };   // committed when the function returns. Note what is ABSENT:
}                                      // nobody sets an owner — `CreatedBy` is stamped for you.

[Page("/notes")]
[Render(CSR)]
component Notes() {
  var notes = Note.ToList();          // a SERVER read. No fetch, no endpoint, no DTO.
  string draft = "";                  // a client value — inferred from the initializer, not declared

  action Add() { Publish(draft); }    // calls the server BY NAME. The engine hands off.

  render {
    Input(value: draft, placeholder: "Title");
    Button("Publish", onPress: Add);
    foreach (var n in notes) { Text(n.Title); }
  }
}
```

That is the entire feature. **There is no other file.** No `NotesController`, no `NoteDto`, no `notesApi.ts`, no
`useNotes()` hook, no repository, no migration, no `[Authorize]` on an endpoint — and `Note.ToList()` returns *your*
notes rather than everyone's, because the `security { }` block is part of the query rather than a check someone
remembered to write.

## Description    {#description}

### What you are NOT building   {#not-building}
If you have built a SaaS app before, you are carrying a mental checklist. Most of it does not apply here, and the time
you would spend on it is the time you should spend on the actual problem:

| What you would normally build | What replaces it |
|---|---|
| REST controllers, routes, an API surface for your own UI | **Nothing.** The UI calls functions by name. |
| DTOs, view models, mappers | **Nothing.** The entity is the wire type. |
| A serialization layer | **Nothing.** One shared codec, for every app. |
| `fetch`/axios, URLs, CORS config | **Nothing.** There is no URL to call. |
| A client store, reducers, cache invalidation | **Nothing.** Reads land in a shared store; a `live` read refetches on a change signal. |
| Change tracking — dirty flags, diffing, a save pipeline | **Nothing.** The platform tracks what you created and changed. You only say **when** to persist (`UnitOfWork.Commit()`), and on the server not even that. |
| An ORM, a repository, a `DbContext` | **Nothing.** You query the entity type directly ([Querying data](https://osysharp.com/reference/query/index/)). |
| Migrations, DDL | **`osy compile`.** Additive change (a new entity, a new field) just applies. See the caveat below. |
| Auth middleware, per-endpoint guards | **`security { }` on the entity** ([The security model](https://osysharp.com/reference/security/index/)) + `[Authorize]` on the page. Written once, enforced everywhere. |
| Session/JWT plumbing | **`Security.IssueJwt`** + `Session.SignIn` ([auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/)). |
| `async` / `await` / `Task<T>` | **Nothing.** There is no `async` in Osy# ([async / await — why Osy# has neither](https://osysharp.com/reference/function/async-await/)). |
| Background jobs, queues, retry logic | **A `workflow`** — durable states that survive a restart. |

The platform's own Admin — a complete app with authentication, grids, tabs and forms — declares
**no API, no DTO, no fetch layer, no store and no migration.** Its entire authentication surface is one file of about
forty lines, most of which are comments.

### It is one model — but it is not one machine   {#the-boundary}
This is where an honest guide has to stop selling. **One language, one call syntax, and a boundary the engine crosses
for you — but three things stay yours to reason about.** Get these three right and you can forget the rest.

**1. Where a statement runs — and, just as importantly, where it does *not*.** Two things reach the server from a UI
action, and only two:

- **A query** — naming an entity (`Note.ToList()`, `Note.Single(…)`). That is the fetch.
- **A call to a top-level function** — `Publish(draft)`. That is a hand-off, and therefore a round trip.

**Everything else is local.** Once rows are in hand they live in the client's store, so reading their properties —
`n.Title`, `n.Owner`, a field on a row you are editing — costs **nothing**. It is not a request; it is memory. So do
not contort a component to "avoid extra reads" of data it already has: there are no extra reads. Pure computation,
control flow and looping over rows you fetched all run where the cursor already is.

What *is* worth noticing is a loop that calls a **function** ten times — that is ten hand-offs. The syntax hides the
round trip; the latency does not.

**2. When data persists.** You never write change tracking. Osy# tracks it all — which rows you created, which fields
you touched, what the new values are — with no dirty flags, no diffing, no "is this modified" bookkeeping, and no save
pipeline of your own. **The only thing you ever say is *when* it should persist**, and even that is only on one side:

- **In a server function**, `new Note { … }` is **persisted when the function returns.** There is nothing to say — no
  `Save()`, no `UnitOfWork.Commit()` ([function](https://osysharp.com/reference/function/declaration/)).
- **In a UI action**, the edit is **staged optimistically** — it appears on screen at once, and persists when a
  **`UnitOfWork.Commit()`** runs, which belongs in a **Save** action.

So the same statement means "write it" on the server and "stage it, and show it immediately" on the client. That is
not an inconsistency to memorise — it is the whole reason a form can be typed into, previewed and abandoned without a
round trip per keystroke. The tracking is done for you either way. You are only ever choosing the moment.

**3. How a page is delivered.** `[Render(CSR)]` is the default and is always correct. `[Render(SSR)]` pre-renders for a
faster first paint — but **only for `[AllowAnonymous]` pages**, so a signed-in page is delivered client-side either
way. Choosing it never changes what a page *does*.

### The flow is durable, which is why the boundary can be invisible   {#durable}
When an action hits a server call, it does not fire a request and wait. It **suspends** — its whole continuation is
persisted — the server runs the function, and the action **resumes mid-statement**. That is why the boundary needs no
syntax: the engine can stop and restart your flow anywhere, so it does not need you to mark where the seams are.

The consequence worth knowing: a suspended flow survives a **client reload and a server restart**. The thing that would
be a distributed-systems problem in the architecture you were about to build is a property of the engine here.

### When you DO write an API   {#when-an-api}
There is a REST surface — `app.Apis` — and an MCP surface — `app.McpServer`. Both exist for exactly one purpose:
**publishing your app to someone else.** A third party's integration. An agent. A partner's webhook.

**Never for your own UI to talk to your own server.** And note what they are: you *expose an existing function*; you do
not write an endpoint. The function stays transport-agnostic — it does not know or care that it is reachable over HTTP.

Calling *someone else's* API is the other direction entirely, and has its own surface ([Http.*](https://osysharp.com/reference/http/facade/)).

### What is left to build?   {#the-work}
What is left, once the scaffolding is gone, is the part that was always the point:

- **The model** — the entities, and what is true about them ([entity](https://osysharp.com/reference/entity/declaration/), [invariant](https://osysharp.com/reference/entity/invariants/)).
- **The rules** — who may see and change what ([The security model](https://osysharp.com/reference/security/index/)). One block per entity, enforced in every query, on
  every path, forever.
- **The logic** — ordinary functions.
- **The screens** — components that name the data they need.

An agent given a business problem should start at the entities and the security rules, and will find that most of what
it expected to build does not exist. That is the intended experience.

### Two caveats: schema evolution, and the UI surface   {#caveats}
- **Schema evolution is not unconditional.** Additive change applies on `osy compile`. A change that would **remove or
  reshape existing data** (a drop, a rename, a type change) is *held* until you authorise it with a generated migration
  — which is the platform refusing to destroy data behind your back, not a gap.
- **The UI surface is still moving.** The `ui/` pages are marked `preview` for that reason. The execution model on this
  page — the hand-off, the security-in-the-query, the absence of an API — is settled; the exact spelling of a component
  member is the part still being sharpened.

## See also       {#see-also}
- [app.osy](https://osysharp.com/reference/project/manifest/) — the `app { }` block: the one file that says what your app is
- [project layout](https://osysharp.com/reference/project/layout/) — where the `.osy` files live
- [The security model](https://osysharp.com/reference/security/index/) — the rules that replace your auth middleware, and why they are in the query
- [Querying data](https://osysharp.com/reference/query/index/) — how you read data, and what it costs
- [function](https://osysharp.com/reference/function/declaration/) — a server function (and why it has no `Save()`)
- [async / await — why Osy# has neither](https://osysharp.com/reference/function/async-await/) — why there is no `async`, and where the one `await` lives
- [publishing a REST API (app.Apis)](https://osysharp.com/reference/api/rest/) — `app.Apis`: publishing your functions to someone else


---

<!-- https://osysharp.com/reference/project/migrating-runs/ -->

# Moving runs onto the version you just deployed

> Deploying a new version does not move the runs already in flight — they keep executing the version they started under. migrate moves them forward, applying whatever each deploy's migration said, and workflow-runs shows you what is still behind.

<!-- id: project-migrating-runs · area: project · stability: stable · html: https://osysharp.com/reference/project/migrating-runs/ -->

## Summary        {#summary}
A run in flight keeps executing the version it started under. That is the guarantee that makes deploying safe while
work is in progress — and it means a deploy leaves runs behind, on purpose.

A deploy moves them forward for you, as its last act. `migrate` is how you finish the job afterwards — for runs a
deploy would not move until you authored a verb for them, for runs parked mid-body, and for a deploy you deliberately
told not to drain. It applies whatever the deploys in between said should happen to each run. `workflow-runs` shows
you what is still behind before you do it, and `--dry-run` shows you exactly what would happen without doing anything.

## Signature      {#signature}
```console
$ osy workflow-runs                # what is in flight, and what is behind
$ osy migrate --dry-run            # decide everything, write nothing
$ osy migrate                      # move them
```

## Description    {#description}

### Seeing what is behind   {#listing}
`versions` answers "why is this app still carrying six schemas" — a count of runs per version. That is the right
question for reclaiming storage and the wrong one after a deploy, because a count cannot tell forty healthy runs from
forty stragglers nobody can move.

`workflow-runs` names the work instead:

```console
$ osy workflow-runs
╭────────────┬──────────────────┬─────────┬──────┬───────────────┬────────╮
│ workflow   │ state            │ status  │ runs │ version       │ oldest │
├────────────┼──────────────────┼─────────┼──────┼───────────────┼────────┤
│ OrderFlow  │ AwaitingPayment  │ waiting │    6 │ 1.2.0 (behind)│ 11d    │
│ OrderFlow  │ Draft            │ waiting │   41 │ 1.3.0         │ 2h     │
╰────────────┴──────────────────┴─────────┴──────┴───────────────┴────────╯
```

Rows that are BEHIND come first and are called out, because they are the ones you can act on.

### The drift `migrate` cannot see: an older revision     {#not-in-current-source}
There is a second way a run drifts from your source, and it is invisible to everything above — because the run never
goes *behind* at all.

While you are developing, a compile edits the current version in place. Change a workflow and the platform mints a new
**revision** of it and keeps the old one, so a run that is parked mid-flight goes on resolving through the revision it
started under. That is deliberate and it works: the run resumes and finishes normally, even if you deleted the state
it is sitting in.

What it means, though, is that the run is executing a definition **you can no longer read**. And since the run is on
the current version, `migrate` correctly reports that there is nothing to move — an answer that is true and reads like
a clean bill of health.

So `workflow-runs` says it outright, and sorts those rows above even the stragglers:

```console
$ osy workflow-runs
╭────────────┬───────────────────────────────────┬─────────┬──────┬─────────┬────────╮
│ workflow   │ state                             │ status  │ runs │ version │ oldest │
├────────────┼───────────────────────────────────┼─────────┼──────┼─────────┼────────┤
│ OrderFlow  │ Draft (not in current source)     │ waiting │    3 │ 1.3.0   │ 20m    │
╰────────────┴───────────────────────────────────┴─────────┴──────┴─────────┴────────╯
Some runs are parked on a state your current source no longer declares. They still run — each keeps the workflow
revision it started under — but they are executing a definition you can no longer read. `migrate` will not report
them: they are already on the current version. Re-add the state, or cancel those runs (`osy cancel-runs`).
```

⚠ A **deploy** does not have this shape: it mints a new app version, and a migration then has to say where every
parked run goes before it can move one. This is a development-loop phenomenon, which is exactly why it needed saying
out loud — it is the one place a run and its source part company with nothing stopping it.

### Moving them   {#migrate}
`migrate` takes every run that is not on the current version, one at a time, under that run's own lock. For each it
applies the [Migrating runs that are still in flight](https://osysharp.com/reference/workflow/migration/) each deploy authored — hop by hop, in order, so a run several versions behind gets
every author's decision rather than a guess made from the endpoints.

A pass has three outcomes and they are reported separately, because they are not variations of each other:

- **moved** — the ordinary case. The run is now resolving the current version's code.
- **ended** — an authored `terminate` stopped it, on the version it was already on, with the reason somebody wrote at
  the time. You are seeing the effect of a decision made weeks ago; the message is the thing to read.
- **refused** — nothing could be decided safely, so nothing was done to that run. The reason names what could not be
  matched and the verb that would answer it. The run is untouched and can still finish on its own version.

### Always dry-run first   {#dry-run}
`--dry-run` decides everything and writes nothing. It is not a partial answer or an estimate — it is the same pass,
with the writes withheld, so the report is exactly what a real run would produce.

That matters most for the two outcomes you cannot undo by re-running: an ended run is ended, and a moved run has left
the version it was on. Reading the report first costs a few seconds.

### Refusals are a normal outcome, not an error   {#refusals}
A refusal means a state, a slot or a deadline in the new version could not be matched by name, and nothing in a
migration file said what to do about it. The run keeps working: it stays where it is, on the version that still
declares everything it needs, and can be completed by whoever is holding it.

The fix is a [Migrating runs that are still in flight](https://osysharp.com/reference/workflow/migration/) in the next deploy, and the refusal names the verb. `compile --generate-migration`
writes the file for you — including a `keep;` for every state nothing happened to, and a placeholder that will not
deploy for anything it could not decide.

### What a move does to a run's deadlines   {#deadlines}
Every live deadline the run holds is re-pointed at the new version's declaration AND re-read against it, so a budget
you changed takes effect on runs that were already waiting. A budget you did not change carries silently, and so does
a declaration you merely moved between a slot, its state and the workflow — that is the same deadline written at a
different level of reach.

Changing a live budget is the one case that cannot be settled here, because it has to be settled at DEPLOY time: the
deploy refuses until the migration says `carry clock <Kind> on <Slot>;` (keep the time already spent) or
`reset clock <Kind> on <Slot>;` (start the new budget from now). See [Migrating runs that are still in flight](https://osysharp.com/reference/workflow/migration/). So by the time you run
`osy migrate`, that decision has already been made and written down.

**A deadline you REMOVED is retired, and the pass tells you.** Deleting a declaration has one reading — the promise
was withdrawn — so the timer stops, the run moves normally, and nothing breaches. A run that can no longer be late is
not a run that failed, and refusing the move would strand somebody's work over a deadline you deliberately deleted.

```console
$ osy migrate
40 run(s) moved:
  OrderFlow  AwaitingApproval  app_v2 → app_v3
1 deadline(s) RETIRED — the new version no longer declares them:
  OrderFlow Finished on Payee — 40 run(s); they keep going, with one fewer obligation. Nothing breached.
```

It is grouped by deadline, not listed per run: the useful number is *which promise stopped, for how many*. Each run
also carries a `Retired` entry on its own timeline, so it is still explicable months later — `osy inspect` and the
Admin Runs tab both read it.

### A deploy already does this for you   {#deploy-drain}
**Since 2026-08-08 a deploy moves the runs it strands, as its last act** — so in the ordinary case you never run this
command at all. A deploy is exactly the moment runs get left behind (they were parked when the version changed under
them), and a follow-up command an operator has to remember is one that does not get run on a scheduled or automated
deploy.

What the deploy reports is what this command would have: how many moved, how many an authored `terminate` ended, and
— as **warnings on the compile** — anything it would not move, grouped by reason. A refusal never fails the deploy:
the version is already minted and live by the time the runs are moved, so a refusal is an outcome to read, not an
error to retry.

It also means an old version is reclaimed by the **same** deploy that drains it, rather than the next one: the runs
move first, and the version garbage collector then finds nothing holding the old schema open.

So you run `osy migrate` for the cases the deploy could not finish:

- you have **authored the verbs** a previous deploy's refusals asked for and want to move those runs now, without
  waiting for another deploy;
- a run was parked **mid-body** (an awaited child or a saga step) and could not be moved yet;
- you deployed with the drain **turned off** deliberately — see below.

### And it keeps trying afterwards   {#retry}
A deploy's drain is one pass, and a single pass does not always finish: a run can fail to move because something else
was holding its data, an old parent can start an old child while the pass is running, and a very large backlog can
outlast the pass's own backstop. So the platform re-runs the pass on a schedule, per app, with no configuration and no
cron of yours. It is the same pass this command runs — there is one answer to "where does this run go", not one for
you and a different one for the background.

**It knows the difference between "not yet" and "not ever", and that is the point.** A run refused because a state has
no counterpart, or because its author has not written `reenter;`, will be refused identically for ever — coming back
in an hour cannot help it. Those refusals are reported once and then counted, and they do not make the sweep hurry.
Only something that could genuinely go differently next time — a run that errored mid-move — brings the next pass
forward.

The practical consequence: **you do not have to babysit a deploy's refusals.** Author the verb whenever you get to it
and deploy; if instead the problem was transient, it will already have cleared itself. Nothing accumulates silently —
`osy workflow-runs` still shows exactly what is behind.

### Deploying now and draining later   {#deferred-drain}
The drain takes each run's own lock and re-bases its clocks, so on an app with thousands of runs parked it is real
work. A deploy can skip it and leave the runs for a controlled window; nothing is lost by waiting, because a parked
run keeps executing against the version it started on — that is the whole point of the versioning model. The only
cost is that the old schemas stay held until you run `osy migrate`.

## Examples       {#examples}
The usual sequence after a deploy that renamed a state:

```console
$ osy workflow-runs
# → 6 OrderFlow runs in AwaitingPayment, on the previous version

$ osy migrate --dry-run
Dry run — nothing was written.
6 run(s) moved:
  OrderFlow  AwaitingSettlement  app  →  app_v2

$ osy migrate
6 run(s) moved.
```

And a pass with something to look at:

```console
$ osy migrate
4 run(s) moved:
  OrderFlow  Draft  app_v2 → app_v3
1 run(s) ENDED by an authored `terminate`:
  OrderFlow in Abandoned on app_v2 → cancel: the offline-payment route was removed in v3
2 run(s) could not be moved:
  OrderFlow on app_v2: slot 'Payer' of state 'AwaitingPayment' has no match in 'app_v3' — it was
  removed or renamed. An authored migration must re-point it (`rename slot Payer -> <New>;`) or drop
  it (`drop slot Payer;`), which cancels its claim explicitly.
```

## See also       {#see-also}
- [Workflows that outlive the code that started them](https://osysharp.com/reference/workflow/change-over-time/) — the model these commands operate on: how a run's position survives a deploy
- [Migrating runs that are still in flight](https://osysharp.com/reference/workflow/migration/) — the file that says where a parked run goes, and how it is generated
- [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/) — what a version is, and which runs are holding one open
- [Stopping runs after a bad deploy](https://osysharp.com/reference/project/cancel-runs/) — the other way out: stop runs on a bad version rather than move them
- [Renaming and removing things that hold data](https://osysharp.com/reference/project/renaming-and-removing/) — the same file's other half, about the DATA a change touches


---

<!-- https://osysharp.com/reference/project/runtime/ -->

# Osyrin, the runtime

> Osy# is the language. Osyrin is the runtime it executes on, and it is the half of your app you did not write.
> 
> This is what it holds itself to — six principles that decide how your program runs — and what stands behind them.

<!-- id: project-runtime · area: project · stability: stable · html: https://osysharp.com/reference/project/runtime/ -->

## Summary        {#summary}

**Osy# is the language. Osyrin is the runtime.**

You write Osy#. Osyrin is what runs it — the database, the web server, the client, the query your security is
compiled into, the durable engine that keeps a workflow alive across a deploy. It is not a framework you call and
not a host you configure. It is the other half of your app, and it is the half nobody has to maintain.

[Hosting an Osy# app](https://osysharp.com/reference/project/hosting/) is about where it runs. This page is about **what it promises while it does**.

## The six principles   {#description}

### 1. You do not place code. The compiler does.   {#placement}

Every function body has a side — browser or server — and it is INFERRED from what the body touches, not declared.
A body that reads a keyboard is the browser's. A body that reads an entity is the server's. A call that leaves
its side is a round trip, and the compiler knows which ones those are even when the source gives no hint.

The consequence: there is no API between your halves, because there are no halves to bridge. See [How an Osy# app works (the execution model)](https://osysharp.com/reference/project/index/).

### 2. Security is part of the query, not a step before it.   {#security}

`security { }` is compiled INTO the read — a row filter and a field mask inside the SQL, under every query your
program can express. Not middleware, not a guard clause, not a repository that remembers.

This is why no Osy# code can bypass it: there is no code path that reaches rows another way. A read returns your
rows because the query only ever asked for yours. See [The security model](https://osysharp.com/reference/security/index/).

### 3. A function is a transaction.   {#unit-of-work}

The writes a function makes land together or not at all. You do not open a transaction, you do not batch, and you
do not think about the ordering — the unit of work is the function.

### 4. Effects that leave the platform run exactly once.   {#exactly-once}

A database rollback does not un-send an email or un-charge a card, so the calls that reach outside are classified
and made durable: they survive a crash and a deploy, and they do not run twice. There is no retry policy to
author. See [Automatic durability (steps you do not have to write)](https://osysharp.com/reference/workflow/automatic-durability/).

### 5. A run outlives the code that started it.   {#versions}

Deploy while a three-day approval is mid-flight and that run finishes on the model it began under, while new runs
use the one you just shipped. Nothing is drained and nothing is replayed against a shape it never saw. See
[Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/).

### 6. What the tool says is what the compiler did.   {#one-answer}

`osy model`, `osy explain`, the editor's diagnostics and the build all read the same front end. A page cannot
disagree with a hover, and a hover cannot disagree with the error you get on compile — not because they are kept
in step, but because there is one of them.

## What stands behind it   {#evidence}

Principles are cheap. These are the numbers, measured on this commit rather than remembered:

| | |
|---|---|
| **Server tests** | **10,768** test methods across **1,644** classes. A `[Theory]` expands to many cases, so the count a run executes is higher still. |
| **Client tests** | **37,628** across 263 files — the browser runtime, the interpreter, the render walker, the durable bridge. |
| **Osy# tests** | **1,086** `[Test]`s in **130** `.test.osy` files — the language's own test framework, testing real apps against a real database. |
| **Documented examples** | **829** fenced examples compiled through the real compiler into a real app by the docs gate. A page cannot teach a spelling the compiler refuses. |
| **Security invariants** | **675** tagged sites — each naming what must hold, its failure mode, and the test that guards it. |
| **Apps in the tree** | **34**, validated on every merge. They are the samples, the demos and the platform's own control plane. |

⚠ **Read that as coverage, not as a maturity claim.** The language and the runtime are early; what these numbers
say is that the surface is exercised, not that it is finished. [[project-hosting#today]] is the honest account of
which hosting shapes ship today.

## What it is not   {#not}

- **Not an interpreter of your source at runtime.** Client code is lowered and runs as client code; server bodies
  execute as resolved trees against the database.
- **Not a framework.** You do not call it, register with it, or implement its interfaces. You declare, and it
  runs what you declared.
- **Not configurable infrastructure.** There is no host to tune, no pool to size, no scheduler to configure. The
  absence is the design, not a gap waiting for a settings file.

## See also   {#see-also}
- [Hosting an Osy# app](https://osysharp.com/reference/project/hosting/) — where it runs, and what you never set up.
- [How an Osy# app works (the execution model)](https://osysharp.com/reference/project/index/) — what you write, and what is already done for you.
- [Automatic durability (steps you do not have to write)](https://osysharp.com/reference/workflow/automatic-durability/) — the exactly-once guarantee, in detail.
- [The security model](https://osysharp.com/reference/security/index/) — the rules that end up inside the query.


---

<!-- https://osysharp.com/reference/project/renaming-and-removing/ -->

# Renaming and removing things that hold data

> Renaming an entity or property, and removing one, are changes to something that already holds rows. Say what you meant in a migration and the rename keeps your data where it is and the removal is recorded rather than forgotten.

<!-- id: project-renaming-and-removing · area: project · stability: stable · html: https://osysharp.com/reference/project/renaming-and-removing/ -->

## Summary        {#summary}
Renaming an entity or a property is not the same kind of change as adding one. To your source it is a small edit; to
your database it is a table full of rows whose name just changed out from under it. A removal is the same problem
seen from the other side — the property is gone from your code, and its data is not.

Neither is guessed at. A deploy that removes something holding data is **refused** until you say what you meant in a
`.migration` — and then it does the safe thing: a rename keeps every row exactly where it is, and a removal is
recorded, so the column it leaves behind has an owner instead of becoming an anonymous leftover.

## Signature      {#signature}
```osy syntax
migration "what changed" {
  from "ast:<hash>";
  to   "ast:<hash>";

  rename entity   Job       -> Assignment;
  rename property Job.Notes -> Job.Remarks;
  drop   property Job.Spare;
  drop   entity   Ghost;
  drop   enum     JobStatus.Parked;
}
```

## Description    {#description}

### What you see if you forget   {#refusal}
Deploy a removal with nothing to authorize it and the deploy stops, having changed nothing:

```text
This deploy removes something that holds data, and nothing says you meant to. Removing an entity or a property is
not a change the deploy will make on your behalf, because the data outlives the source: the rows stay in the
database after the declaration is gone. Offending change(s): entity 'Job' removed.
Say what you meant in a migration and deploy again:
  drop entity Job;              // or: rename entity Job -> <NewName>;
```

The reason it asks rather than guessing is that a removal and a rename look **identical** from the outside — both are
just "this name is gone" — and they want opposite handling. Guess wrong in either direction and you lose data: treat a
rename as a removal and every row is stranded in a table your app can no longer name; treat a removal as a rename and
an unrelated table's rows are silently adopted.

### You do not have to write this by hand   {#generating}
Ask for it, and the file is generated from the difference between what is deployed and what you have now:

```console
$ osy compile --generate-migration
```

The generated file carries only the changes that need your word — the additive ones are simply applied. Where it can
tell a rename from a genuine removal it writes the `rename` for you and says so:

```osy syntax
rename entity Job -> Assignment;   // INFERRED: 75% of its properties match by name and type. Verify — if these are
                                   // genuinely different entities, replace this with `drop entity Job;`
```

It only writes a `rename` when there is one obvious answer. If two properties of the same type left and two arrived,
or a removed entity resembles several new ones equally, it will not guess — it lists the candidates as a comment and
leaves the `drop` in place for you to correct. That reticence is deliberate: a rename you did not mean adopts an
unrelated table's rows, which is worse than the removal it replaced.

Review the file, fix anything it guessed wrong, and deploy with it:

```console
$ osy compile --migration migrations/9f00abcd.migration --new-version           # local
$ osyrin app compile --migration migrations/9f00abcd.migration --new-version    # a deployed app
```

### What a rename actually does — which is nothing, physically   {#rename}
Nothing is copied, nothing is moved, and no table is renamed. Your app's description of its data is versioned; the
data itself is shared. So the new name simply *points at the same table*:

- the version you just deployed knows the entity as `Assignment`;
- a run still finishing on an older version knows it as `Job`;
- both read and write the same rows, at the same time, correctly.

A physical rename would be the wrong tool for exactly that reason — it would break every run still asking for the old
name. Renaming a property works the same way and for the same reason.

This holds through repeated renames. `Job` → `Assignment` → `Task` still reads and writes the rows you created on day
one; each version just calls them something different.

### What a rename saves you from   {#rename-why}
Without one, the change reads as a removal plus an addition, because that is all there is to see. You would get a new,
empty `Assignment` and every existing row left behind in a `Job` your app can no longer name. Nothing is deleted — but
nothing is reachable either, and the app comes up looking like it forgot everything. That is the outcome the refusal
exists to stop.

### What a removal actually does   {#removal}
The property disappears from your app immediately — it is gone from the model, from queries, from screens. Its
**column stays**, because a run still finishing on an older version may go on reading and writing it, and that is the
whole point of versioning.

What is new is that the removal is **recorded**. The version you just deployed keeps a note saying "this property
existed and stopped here". That record is what later lets the column be reclaimed safely, and it travels forward: two
deploys later, the record still says the property died in the version where it actually died.

The practical effect is that a leftover column is never confused with a mystery column. When space is eventually
reclaimed, only columns with a recorded removal are taken. Anything else that turns up on a table — the residue of a
hand-run `ALTER`, a half-finished change — is reported to you and left exactly where it is. Deleting a column nobody
can account for is not a decision worth making automatically.

### Removing a required property   {#removal-required}
Its column stops being required, because it has to: the version you just deployed does not know about the property,
so it has nothing to put there, and rows created from now on simply leave it empty. A run still pinned to an older
version will therefore find that value missing on rows created after the removal. Finish moving your readers and
writers over before anything depends on it being filled in.

### Removing an enum member is the sharpest case     {#enum-member}
An enum member is not stored by its name. A column holds the member's **position**, so removing one **re-numbers every
member after it** — and no existing row is rewritten. `{ Queued, Parked, Done }` minus `Parked` is `{ Queued, Done }`,
and every row that said `Parked` now says `Done`.

That is worse than the stranding above, and quieter. A dropped property leaves data unreachable, which you notice. A
dropped enum member leaves data perfectly reachable and **meaning something else**, which you do not.

So it is treated exactly like the other removals: a deploy that removes a member is refused until a migration says you
meant it, and the remedy names the member:

```osy syntax
migration "retire the parked state" {
  drop enum JobStatus.Parked;
  // …or, if the member was really renamed:
  rename enum JobStatus.Parked -> JobStatus.Held;
}
```

A **rename** is the interesting one, and it is why the two are separate verbs. Renaming a member keeps its position,
so every stored row goes on meaning what it always meant, under the new name. That is almost always what you wanted.

⚠ **While you are developing, the local compile does not refuse — it warns.** `osy compile` edits the version in place
rather than deploying a new one, so it applies the change and tells you what it did to your data:

```text
enum member 'JobStatus.Done' changed its stored value from 2 to 1 — usually because an earlier member was removed
and the rest re-indexed. Existing rows are NOT rewritten, so rows holding 1 now read as 'Done', and rows written
when 'Done' meant 2 no longer do.
```

Read it rather than scrolling past it: the rows in your development database now say something you did not write. The
warning appears whenever a member's position moves — a removal, or a reorder — and stays silent when you simply add a
member at the end, which moves nothing.

### One thing you cannot do   {#refused}
Changing the stored **type**, **width** or **precision** of a property that already has data is refused. There is no
shape a column can take that is simultaneously the old type for a run still using it and the new type for the code
you just deployed. Do it additively instead — add a new property, copy the data across, then remove the old one once
every reader **and writer** has moved. See [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/) for the full sequence and its warning.

## Examples       {#examples}

Renaming an entity and one of its properties in the same deploy:

```osy title="before" test app=renaming
entity Job {
  [Required, MaxLength(200)] string Title;
  [MaxLength(400)] string? Notes;
}
```

```osy title="after" test app=renaming-after
entity Assignment {
  [Required, MaxLength(200)] string Title;
  [MaxLength(400)] string? Remarks;
}
```

The migration that says so:

```osy title="the migration that carries the data across" syntax
migration "job becomes assignment" {
  from "ast:<the deployed hash>";
  to   "ast:<the new hash>";

  rename entity   Job       -> Assignment;
  rename property Job.Notes -> Job.Remarks;
}
```

Deploy it and every `Job` you ever created is an `Assignment`, with its notes intact under the new name. Runs that
were already in flight go on calling it a `Job` until they finish.

## See also       {#see-also}
- [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/) — why an older version is still reading your data, and how versions are cleaned up
- [Stopping runs after a bad deploy](https://osysharp.com/reference/project/cancel-runs/) — stopping runs that are still executing a version you want rid of
- [entity](https://osysharp.com/reference/entity/declaration/) — declaring the entities this is all about
- [enum](https://osysharp.com/reference/enum/declaration/) — declaring the enums whose members re-number when one is removed


---

<!-- https://osysharp.com/reference/project/cancel-runs/ -->

# Stopping runs after a bad deploy

> A workflow run keeps executing the version it started under, which is exactly what you want until that version is the problem. cancel-runs stops every run still executing a given version, without running their compensation logic.

<!-- id: project-cancel-runs · area: project · stability: stable · html: https://osysharp.com/reference/project/cancel-runs/ -->

## Summary        {#summary}
A run in flight goes on executing the version it started under — deliberately, and permanently. That is the guarantee
that makes deploying safe while work is in progress.

It is also a trap the first time you ship something broken. You notice, you deploy the fix, and the fix only applies to
*new* runs: every run already started keeps executing the broken code to completion. `cancel-runs` is how you stop them.

## Signature      {#signature}
```console
$ osy cancel-runs --version <version>           # local
$ osyrin app cancel-runs <version>              # a deployed app
```

## Description    {#description}

### What it does   {#what}
Every workflow run still in flight against the named version is stopped: marked cancelled, recorded in the run's audit
timeline as such, and any parked continuation discarded so nothing can wake it again. Runs on other versions are
untouched.

Find the version with [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/) — the `In flight` column is what you are about to cancel:

```console
$ osy versions
$ osy cancel-runs --version 1.1.0
```

It tells you how many runs you are about to stop and asks before doing it. `--yes` skips the prompt.

### It stops whole sagas, not single runs   {#sagas}
A saga is a tree: a parent waiting on a child, which may itself be waiting on another. Cancelling one run of that tree
cancels **all** of it, whichever run you name.

That is not overreach, it is the only coherent option. Cancelling just the child would leave the parent waiting forever
on something that can never finish — one stuck run traded for another. Cancelling just the parent would leave children
doing work whose result nothing will ever read.

### It does not run your compensation logic   {#no-compensation}
This is the important one, and it is deliberate.

When a saga fails normally, its `catch` runs and compensates — undoing what it did. **Cancelling does not do that.** The
reason you are reaching for `cancel-runs` is usually that the deployed code is wrong, and your compensation logic *is*
that same deployed code. Running it would execute the very thing you are trying to escape, and on a saga that is already
in a state you did not intend.

So: **side effects those runs already performed remain performed.** Money moved stays moved, emails sent stay sent. The
audit says the run was cancelled, not that it was compensated, so the record does not claim an unwind that never
happened. Undoing that work is yours to do deliberately — with full knowledge of what actually ran.

Cancelling is a blunt instrument on purpose. It is the tool for "stop, this is wrong", not for "unwind this cleanly".

### It also lets the version be cleaned up   {#reclaiming}
Runs in flight are exactly what stops an old version being reclaimed. Once you have cancelled them, nothing holds that
version open and the next deploy reclaims it — so the broken version stops appearing in `versions` too.

### Who can run it   {#permission}
The same permission as deploying to the app. Whoever can ship the bad version can stop it.

## Examples       {#examples}

The whole sequence, from noticing to clean:

```console
# 1. What is running, and against which version?
osy versions
# → 1.2.0  app_v3  14 in flight  oldest 09:12 (2h ago)  held by runs in flight
#   1.3.0  app_v4   0            —                      current

# 2. Deploy the fix. New runs are fine from here; the 14 are not.
osy compile --new-version

# 3. Stop the runs still executing the broken version.
osy cancel-runs --version 1.2.0
# → About to cancel 14 run(s) in flight against version 1.2.0 (app_v3).
#   No compensation runs. Side effects those runs already performed will remain.
#   Continue? [y/n]

# 4. Nothing holds app_v3 now, so the next deploy reclaims it.
osy versions
```

## See also       {#see-also}
- [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/) — why a run keeps its own version in the first place, and how to see what is in flight
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — the parent/child await that makes a saga a tree


---

<!-- https://osysharp.com/reference/project/statistics/ -->

# Usage statistics (what is sent, and the two ids that let us count you once)

> The toolchain sends usage counts — command names, versions, OS, error codes and durations — with two random ids: one per installation, so a person is counted once, and one per project, so a started app can be followed to its first deploy. Never your source, your data, your paths, your name or your address. One command turns it off, another resets the ids, and the first run tells you it is on.

<!-- id: project-statistics · area: project · stability: preview · html: https://osysharp.com/reference/project/statistics/ -->

## Summary   {#summary}

The toolchain sends usage counts so that the verbs people reach for, the versions they run and the errors they hit
can be counted — and so that the people trying it can be counted once, and an app somebody starts can be followed
to the moment it first deploys. Two random ids make that possible, and this page says exactly what they are and
what they are not. It never sends your source, your data, your paths, your name, your machine name or your
address. `osy telemetry off` stops it, durably; `osy telemetry reset` replaces the ids with new ones; and the
first run of the toolchain says that it is on and how to turn it off.

## Signature   {#signature}

```console
osy telemetry off      # stop sending, durably
osy telemetry on       # start again
osy telemetry reset    # new installation id — the old one's events can no longer be joined to yours
osy telemetry status   # the setting, where it is stored, your version and the newest one the receiver has named
```

## Description   {#description}

**What is sent.** Each run of an `osy` or `osyrin` verb records one event, locally, immediately — sending it is a
separate step (see "Sent in batches" below):

| field | example | why |
|---|---|---|
| the verb | `launch`, `check`, `init` | which parts of the toolchain are used at all |
| the toolchain version | `0.9.3` | whether a report is about the current release |
| the operating system and architecture | `darwin/arm64` | which builds have to exist |
| the exit code, and an error code when there is one | `CommandParseException` | which refusals people hit most, so their wording can be fixed |
| the duration | `2140 ms` | which verbs are slow |
| the installation id | `9f1c…` (32 random hex characters) | so a person is counted once — see "Two random ids" below |
| the project id | `4a7e…`, when the verb ran inside a project | so a started app can be followed to its first deploy |

**What is never sent.** Your source. Your data. File or directory paths. App names, entity names, function names,
or any other identifier from your project. Your user name, machine name, IP address or e-mail. The text of a
diagnostic (only its type — the class of what went wrong, e.g. `CommandParseException`, never the message that
came with it, which can quote back whatever the command was given). Anything typed into a prompt.

**One deliberate, narrow exception: what you typed into `osy docs`.** A search term for the reference is not a
prompt and not project data — it is a question about the toolchain itself — but it is still text you typed, so it
travels as its own separate, explicitly-named field rather than being folded silently into the general shape:
what you asked (`"Session.SignOut"`), and whether the reference answered it (`Page`, `Ambiguous`, `Miss`, …). This
is the field that turns "the reference sometimes fails" into "the reference fails on THESE terms" — the same gap
`osy docs --misses` cannot see on its own, because that command only ever looks at one machine's own local log.

**Sent in batches, not on every run.** The local event above is free — it is a few bytes on disk. Actually sending
anything anywhere is rate-limited to happen at most a few times an hour, at the very most, whichever run happens
to be the one due — a session running many commands back to back does not generate a burst of network traffic for
each one. A send that fails (the destination is unreachable) is not retried sooner; it waits for the next window,
the same as a successful one would.

**Two random ids, disclosed.** Each event carries an **installation id** — a random value written to
`~/.osy/config.json` the first time the toolchain runs — and, from a project, a **project id** — a random value
`osy init` writes into that project's `osyrin.json`. Neither is derived from anything: not your hardware, your
user name, your e-mail or the project's name. They exist for two questions the counts alone cannot answer: *how
many people* are trying the toolchain (one installation id, counted once, however many runs), and *does an app
that gets started get finished* — a project id joins that project's `init`, its first green `osy check`, its first
`osy launch` and its first deploy into one story, and the time between them. From the two together: projects per
person, and how far the typical person gets. **What they are never used for:** joining to a person. There is no
name, address or account anywhere in the data to join them to, and nothing in the toolchain sends one. Because a
random id that persists is pseudonymous rather than anonymous, this page says so rather than calling the data
anonymous: it identifies an installation, not a human, and `osy telemetry reset` breaks even that link whenever
you like. Raw events are kept for ninety days and then only the weekly counts remain.

**Where people use it — counted by region, never stored by event.** The receiving server learns where a batch
came from and adds it to a count: this region, this week, this verb. The country and its first subdivision are
kept — a US state, a Swedish county, a German Land — because a map of where the toolchain is used is one of the
things the statistics exist for, and "United States" says much less than fifty states do. The connection's address
is used for one lookup at the edge and discarded before anything is stored; where the ingress sits behind a CDN
the location arrives as a header and the address is never seen at all. What is stored is the count, not the event:
there is no row that says one run happened in one place at one time, so there is nothing that could be traced to a
person however few people a region has. The "never your IP address" above stays literally true — it is not a
field, it is not stored, and it is not sent by the toolchain.

**Where it goes, and for how long.** Batches are sent at most once an hour — and right away after `osy init`,
`osy launch` and a deploy, the three moments the funnel is made of, so a first try that goes no further still
counts — to `osysharp.com`, or to whatever
`OSY_TELEMETRY_URL` names — a self-hosted platform receives its own toolchain's statistics through the same route
(`/api/telemetry/v1`), so an organisation that runs its own can keep them entirely in-house. The receiver keeps
three things: the raw events for ninety days and then deletes them; one row per installation id and one per
project id (first seen, last seen, the funnel's four timestamps, the region); and the weekly counts, which are all
that remains after the ninety days. A batch that cannot be sent — no network, the receiver down — waits on disk
for the next hour and costs the command that tried nothing beyond a three-second cap; the waiting log is bounded,
and past a few thousand lines the oldest are dropped.

**The update check is a separate request, to the release channel, and it is anonymous to us.** Once a day the
toolchain asks GitHub's releases page for `osysharp/cli` — the same place Homebrew, winget and the install script
download from — which version is the newest. GitHub sees a request with the toolchain's user agent and nothing
else; the statistics receiver is not involved and never learns who asked. When the answer is newer than the one you
are running, the toolchain prints one line, once a day, before the command's own output:

```console
a newer osy is available: 0.9.1 (you have 0.9.0) — osy upgrade
  what's new: osy whats-new   ·   https://github.com/osysharp/cli/releases/tag/v0.9.1
```

`osy upgrade` is the one command that installs it on every platform — through the same download door, checked
against the release's checksums, swapped in place ([Upgrading the toolchain](https://osysharp.com/reference/local/upgrading-the-toolchain/)). `osy whats-new` prints the
release's notes in the terminal, from the same public page, and `osy whats-new 0.9.1` reads a particular version's. It stays on when statistics are off,
because it is not a statistic; `"updateCheck": false` in `~/.osy/config.json`
turns it off for a machine that wants no request it did not type, and `osy telemetry status` shows the running and
the latest published version either way.

**Disclosed at first run.** The first time the toolchain runs it prints one line saying that usage statistics
are on, that they carry a random installation id, and that `osy telemetry off` stops them. It does not ask a question, because a question in
the middle of `osy init` is exactly what nobody reads; it tells you, once, where you will see it.

**Where the setting lives.** `~/.osy/config.json`, in the toolchain's own configuration directory, not in your
project — so it is a choice you make once per machine, and it is not something a checkout can quietly turn back
on. `osy telemetry off` stops LOCAL RECORDING too, not only sending — turning it off means the toolchain stops
writing these events at all, not just stops mailing them.

**Feedback is separate, and it is yours to send.** `osy feedback` opens a prefilled issue on the toolchain's
public tracker ([github.com/osysharp/cli](https://github.com/osysharp/cli)), with your version and the last
diagnostic the toolchain hit — an internal fault, or the last "✗" refusal a command printed — already filled in — a real GitHub issue form you still review and edit before you
submit it. That is a message you write and send; it is not a statistic, and turning statistics off does not
affect it. Pass `--print-url` to see the link without opening a browser.

**`osy feedback --full` adds a diagnostic bundle, and the issue never holds it.** The bundle is a zip built on
your machine: the last diagnostic, the tail of the local run log, the project's `osyrin.json` and `osyrin.lock`
and a list of its files, and the log tails of the local platform serving it. Never your `.secrets`, never an
operator key, never the database. Your `.osy` sources go in only with `--with-source`. The contents are listed
before anything leaves the machine, and then the zip is uploaded **privately** to osysharp.com — into storage
only the maintainers can read — and the issue carries a short bundle id in its place. Anything attached to an
issue on a public repository is public, which is why the file is never attached. `--no-upload` keeps the zip on
disk instead, for you to attach or mail yourself.

```console
$ osy feedback --full "validate refuses a valid app"
diagnostic bundle — 14,212 bytes, 9 entries:
  manifest.txt
  toolchain.json  (131 bytes)
  last-diagnostic.json  (188 bytes)
  …
bundle uploaded privately as fb-3f9a2c17d0 — only the maintainers can read it; the issue carries the id.
Opening a prefilled issue on osysharp/cli — review it before you submit.
```

## Examples   {#examples}

```console
$ osy telemetry status
anonymous statistics: on
  sends: verb · version · os/arch · exit code · error code · duration (+ the search term, for `osy docs` only)
  never: source · data · paths · names · addresses · anything else typed
  ids:   a random installation id · a random project id — see above
  off:   osy telemetry off

$ osy telemetry off
anonymous statistics: off (stored in ~/.osy/config.json)

$ osy feedback "the docs page for X is wrong about Y" --print-url
https://github.com/osysharp/cli/issues/new?title=…&body=…
```

## See also   {#see-also}

- [Hosting an Osy# app](https://osysharp.com/reference/project/hosting/) — where an app runs, and the three places it can
- [The local loop (running your app on your own machine)](https://osysharp.com/reference/local/index/) — the local toolchain, verb by verb


---

<!-- https://osysharp.com/reference/project/manifest/ -->

# app.osy

> The manifest at the root of every project. It names the app, says which files are model, seed, migrations and tests, and declares the capabilities the app depends on with use.

<!-- id: project-manifest · area: project · stability: stable · html: https://osysharp.com/reference/project/manifest/ -->

## Summary        {#summary}
`app.osy` is the manifest at the root of a project. It names the app, says which files play which role, and declares
the **capabilities** the app depends on. It is the first file to read in an unfamiliar project — it tells you what the
app is made of before you open a single model file.

## Signature      {#signature}
```osy syntax
app <Name> {
  model      "model/**/*.osy";        // entities, functions, security, UI
  seed       "seed/**/*.osy";         // data the app needs in order to exist
  migrations "migrations/**/*.migration"; // explicit schema + data migrations — NOT .osy: a migration is a
                                          // generated artefact in its own grammar, so no *.osy glob reaches one
  tests      "tests/**/*.osy";        // [Test] functions — run, never deployed

  use <Capability>;                   // a platform capability this app depends on
}
```

## Description    {#description}

### The source roles   {#roles}
Each role is a glob. They are not decoration — they decide what happens to the file:

| Role | What the files hold | What the platform does with them |
|---|---|---|
| `model` | entities, functions, security, UI | compiled and **deployed** |
| `seed` | the data the app needs to exist at all | compiled and **deployed** |
| `migrations` | explicit schema and data migrations | found here, then passed to a deploy with `--migration` — a `*.migration` is **not** compiled with your model |
| `tests` | `[Test]` functions | **run, never deployed** |

Omit a role and the conventional folder is used, so a manifest can be very short. Being explicit costs one line and
tells the next reader exactly where things live.

### A file belongs to one role   {#one-role}
Two globs can reach the same file. In a **flat project** — every `.osy` in the project root, no `model/` folder — the
obvious manifest does it to every test:

```osy title="a flat project: `*.osy` also matches `*.test.osy`" syntax
app Shop {
  model "*.osy";
  tests "*.test.osy";       // every file here is ALSO matched by the model glob above
}
```

This works, and the narrower glob wins: **where one role's files are a subset of another's, the shared files belong
to the narrower role**. Above, `checkout.test.osy` is a test and nothing else — it is not compiled into the app and
never reaches a deploy.

The rule is about which set is smaller, not about which role is called `tests`, so it reads the same way round:
`model "model.osy"; tests "*.osy";` gives `model.osy` to `model`.

When neither role's files contain the other's, there is no narrower one to prefer and the manifest is **rejected**,
naming both roles and the files. Narrow one of the globs so each file is claimed once — guessing on your behalf is
how a test quietly becomes part of the app.

### `use` declares a dependency   {#use}
`use` opts the app into a **capability** — a piece of platform surface that is not on by default, like outbound HTTP
or searchable text. It belongs in the manifest, because it is a fact about the *application*, not about one file:

```osy title="an app that makes outbound HTTP calls" test app=project-manifest
app Shop {
  model "model/**/*.osy";
  tests "tests/**/*.osy";

  use Osysharp.Http;         // now Http.Get / Http.Post exist for this app
}

string Ping(string url) {
  var r = Http.Get(url);
  return r.IsSuccess ? r.Body : "";
}
```

Without the `use`, `Http.Get` is not a thing the app can call, and the compiler says so. That is deliberate: an
application's ability to reach the outside world should be a line you can point at, not an accident of an import.

Do not confuse `use` with `using`. **`use` (manifest) declares the dependency; `using` (a file) imports its names into
that file.** A version pin belongs on the `use`. See [use](https://osysharp.com/reference/types/use/).

### A minimal manifest   {#minimal}
```osy title="the smallest useful manifest" test app=project-manifest-min
app Notes {
  model "model/**/*.osy";
  tests "tests/**/*.osy";
}

entity Note {
  [Required] string Title;
}
```

That is a complete application: it has a name, a model, and tests. Everything else is added when you need it.

## See also       {#see-also}
- [project layout](https://osysharp.com/reference/project/layout/) — the folders the manifest's globs point at
- [use](https://osysharp.com/reference/types/use/) — `use` vs `using`, and version pins
- [Compiling your app](https://osysharp.com/reference/local/compiling-your-app/) — `osy compile`, which reads this manifest


---

<!-- https://osysharp.com/reference/project/package/ -->

# package.osy

> The manifest that makes a git repository a publishable Osy# package. It names the package, says which of the repository's files actually ship, declares the platform floor its source needs, and lists the packages it builds on.

<!-- id: project-package · area: project · stability: preview · html: https://osysharp.com/reference/project/package/ -->

## Summary        {#summary}
`app.osy` describes an **application**: everything a compile consumes, tests included. `package.osy` describes an
**artifact**: what goes into the archive somebody else fetches. A repository carries tests, CI, docs and
`node_modules`; none of those are the package, and the manifest's globs are where that line is drawn.

A package's identity is `Owner.Name`, and the owner is what makes the name yours — it maps to the GitHub account or
organisation that publishes it, so nobody can publish under a name they do not control.

> **Preview.** The manifest parses and validates today. Fetching, publishing and resolving a package against it are
> being built, so a `package.osy` you write now is checked but not yet consumed.

## Signature      {#signature}
```osy syntax
package <Owner>.<Name> {
  version     "1.0.0";        // required — what a `use` constraint resolves against
  minPlatform "0.9.0";        // the platform floor this package's SOURCE needs
  contract    3;              // the control ABI generation, a separate axis
  summary     "One line.";

  model    "model/**/*.osy";  // the Osy# a consumer's compiler reads
  controls "controls/**";     // built control bundles and their chunks
  fonts    "fonts/**";

  use <Owner>.<Other>@1;      // a package this one builds on
}
```

## Description    {#description}

### What a package says about itself   {#settings}

| Setting | Literal | What it decides |
|---|---|---|
| `version` | string | **Required.** The version a `use Owner.Name@1;` constraint resolves against, and the git tag publishing creates. A package without one cannot be depended on. |
| `minPlatform` | string | The oldest platform this package's source will run on. Read **first**, before anything else in the manifest — see below. |
| `contract` | whole number | The control ABI generation the package's controls are written against. Independent of `version`: a package can ship many versions against one ABI. |
| `summary` | string | One line, shown when somebody is deciding whether to depend on you. |

A setting is declared once. A **role** may repeat, and every glob applies — that difference is the whole reason the
two are separate vocabularies.

### Which of the repository’s files ship   {#roles}

Each role is a glob, and the globs are what decides what ships. Declare a role and your globs replace its default.

| Role | Default | What it carries |
|---|---|---|
| `model` | `model/**/*.osy` | The Osy# a consumer's compiler reads. A package that ships none declares nothing. |
| `controls` | `controls/**` | Built control bundles and their chunks. |
| `fonts` | `**/fonts/*.woff2` and the other three web font formats | Font files, pinned by hash. |
| `icons` | `**/icons/*.svg` | Build-time glyphs, exactly as in an app. |
| `svg` | `**/art/*.svg` | Illustrations rendered inline, keeping their own fills. |
| `textures` | `**/textures/*.png` and the other raster formats | Raster assets. |
| `sounds` | `**/sounds/*.mp3` and the other audio formats | Audio assets. |

There is no `tests` role, and that is deliberate: a package's tests are its repository's business and run before an
archive exists. They are not part of what somebody downloads.

Nothing assumes your sources live under `src/`. A flat repository is a normal package — say so and it works:

```osy preview
package Osysharp.Charts {
  version "1.0.0";
  summary "Composable charts, in Osy#.";

  model "*.osy";
  model "vocabularies/*.osy";
}
```

### A control and its bundle travel together   {#controls}

A control is two halves: a `control` declaration in your Osy#, and the JavaScript bundle it names. They go into one
archive under one hash, so a consumer cannot end up with one half from one build and the other from another. That is
not a rule anybody has to remember — the halves are not two artifacts.

```osy preview
package Someone.Editor {
  version     "2.0.0";
  minPlatform "1.0.0";
  contract    3;

  model    "model/**/*.osy";
  controls "controls/**";
  fonts    "fonts/**";
}
```

### Depending on another package   {#use}

`use` inside a package manifest means what it means inside `app.osy`: this is a dependency, at this version. Ask for
a package and its own dependencies come with it.

```osy preview
package Someone.Dashboard {
  version "1.4.0";
  model   "model/**/*.osy";

  use Osysharp.Charts@1;
  use Someone.Editor@2;
}
```

**A dependency is transitive; an import is not.** Fetching `Someone.Dashboard` fetches `Osysharp.Charts` too, because
the dashboard needs it to compile. It does **not** put the charts in *your* app's scope. If a page of yours names a
chart directly, your own `app.osy` says `use Osysharp.Charts@1;`. That is C#'s rule — a `using` never re-exports — and
it is what stops an app quietly depending on something it never declared, then breaking the day a package drops it.

### minPlatform is read first, and that is not an implementation detail   {#min-platform}

A package published against a **newer** platform carries fields an older one has never heard of. If the older
platform checked the vocabulary first, it would complain about one of those — which reads like a broken package and
is actually an out-of-date reader.

So `minPlatform` is understood before anything else in the manifest is interpreted. An old platform meeting a new
package says the one useful thing:

```console
package 'Someone.Future' needs platform >= 2.0.0; this platform is 1.4.0.
Upgrade the platform, or use a version of the package published for this one.
```

Once the floor is met, the unknown entries are the story again and each is named.

### Where the file goes   {#location}

`package.osy` sits at the root of the package's repository, and it is the only file a `package` block belongs in.
Writing one inside an app's `model/` is refused, because nothing in an application reads it:

```console
a `package` declaration belongs in `package.osy` at the root of the package's repository, not in `m.osy`.
```

## See also       {#see-also}
- [app.osy](https://osysharp.com/reference/project/manifest/) — `app.osy`, the same idea for an application
- [project layout](https://osysharp.com/reference/project/layout/) — where files go in a project
- [use](https://osysharp.com/reference/types/use/) — `use` and version pins


---

<!-- https://osysharp.com/reference/project/layout/ -->

# project layout

> The shape of an Osy# project — a manifest at the root, and folders for model, seed, migrations and tests. What you put where decides what gets deployed and what only ever runs.

<!-- id: project-layout · area: project · stability: stable · html: https://osysharp.com/reference/project/layout/ -->

## Summary        {#summary}
An Osy# project is a manifest and four folders. The folders are not a style preference — they decide what is deployed
and what merely runs, so putting a file in the wrong one has consequences.

## Signature      {#signature}
```console
app.osy          the manifest — what compiles, what is tested, which capabilities the app uses
model/           entities, functions, security, UI       (compiled and deployed)
seed/            data the app needs in order to exist    (compiled and deployed)
migrations/      explicit schema and data migrations     (passed to a deploy, not compiled)
tests/           [Test] functions                        (run, never deployed)
```

## Description    {#description}

### Why the split matters   {#why}
`model/` and `tests/` are the two you will use every day, and the line between them is the one that matters: **tests
are never deployed.** A `[Test]` function lives in `tests/`, runs against a throwaway clone of the app, and never
reaches production — so a test may create rows, break rules and assert on the wreckage without any of it mattering.

Anything in `model/` **is** the application. If you put a test helper there, you have shipped it.

### Splitting model/ up   {#splitting-model}
`model/` is a glob, so its internal shape is yours. One file per area reads well and keeps a diff small:

```console
model/
  orders.osy        entities + the functions that act on them
  customers.osy
  security.osy      the security rules, in one place you can review
  ui/               components and pages
```

There is no required file naming and no ordering rule — the compiler reads the whole model as one unit, so a function
in one file may freely reference an entity declared in another.

### Seed vs migrations   {#seed-vs-migrations}
They are easy to confuse and they answer different questions.

- **`seed/`** — data the app cannot exist without: the roles, the statuses, the country list. It is re-applied to
  make the app *be what it says it is*, so it must be safe to run repeatedly.
- **`migrations/`** — a one-time, explicit change to an existing deployment: a column that needs backfilling, a
  non-additive schema change you have reviewed and authorised, or where a workflow run parked in a state you changed
  now stands.

If you find yourself wanting to "just seed" a production fix, you want a migration.

A `*.migration` is **not compiled with your model**, and it could not be: it is written against one PAIR of
versions and applied once, so re-applying it on every compile is exactly what must not happen. You hand it to the
deploy that needs it — `osy compile --migration migrations/<name>.migration` — and
`osy compile --generate-migration` writes it here for you to review first.

## See also       {#see-also}
- [app.osy](https://osysharp.com/reference/project/manifest/) — the `app.osy` that names these folders
- [Compiling your app](https://osysharp.com/reference/local/compiling-your-app/) — `osy compile`, which compiles model/ and seed/, and takes a migration with `--migration`
- [Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) — running what is in `tests/`


---

<!-- https://osysharp.com/reference/query/collections/ -->

# Child collections (navigating a relation)

> A parent's child collection — `order.Lines` — is not a loaded array. It is a QUERY, correlated to that parent, and every LINQ verb works on it: filter it, sum it, ask if any child matches. Which is why you navigate to children through the parent rather than querying the child table with a foreign-key filter: the collection already knows which parent it belongs to, and the compiler writes that condition for you.

<!-- id: query-collections · area: query · stability: stable · html: https://osysharp.com/reference/query/collections/ -->

## Summary        {#summary}
A child collection is a **query**, not a loaded list:

```osy title="a collection is a query, correlated to its parent" test app=query-collections
entity Order {
  [Required, MaxLength(40)] string Code;
  [ForeignKey(Order)] Line[] Lines;          // the collection
}

entity Line {
  [Required] Order Order;                     // the back-reference that defines it
  [MaxLength(60)] string Sku;
  int Qty;
  decimal Amount;
}

decimal BigLinesTotal(Order order) {
  return order.Lines
              .Where(l => l.Qty > 1)
              .Sum(l => l.Amount);            // ONE SQL statement, correlated to THIS order
}
```

`order.Lines` means *"the lines whose `Order` is this order"* — and because that condition is implicit, everything you
chain onto it is added to it. The `Where` above narrows a set the database has not yet built.

## Signature      {#signature}
```osy syntax
parent.Children                              // a query: the children of THIS parent
parent.Children.Where(c => p)                // …narrowed
parent.Children.Count()  /  .Any(c => p)     // …counted / tested
parent.Children.Sum(c => v)                  // …aggregated (0 over no children — see query-aggregates)
parent.Children.OrderBy(c => k).ToList()     // …ordered and materialised
foreach (var c in parent.Children) { … }     // …iterated
```

## Description    {#description}

### Is `order.Lines` a fetched array, or a query?   {#a-query}
Reading `order.Lines` does not hand you an array that was fetched earlier. It hands you a **question**, which is
answered when you ask it. Three things follow:

- **Everything you chain is pushed into the database.** `order.Lines.Where(l => l.Qty > 1).Sum(l => l.Amount)` is one
  statement with a `WHERE` and a `SUM` — not "fetch all the lines, then filter and add them up in memory".
- **`.Count()` does not fetch the children.** It counts them. A parent with 10,000 lines costs the same to count as
  one with three.
- **Touching it in a loop over parents is N+1.** That is exactly what [`Include`](https://osysharp.com/reference/query/include/) is for — pre-load
  the children with the parents and the navigation becomes free.

### Go through the parent, not around it   {#through-the-parent}
You can always ask the child table directly, and sometimes it is genuinely what you want:

```osy syntax
order.Lines.Where(l => l.Qty > 1)            // ✅ navigate — the parent condition is implicit
Line.Where(l => l.Order == order && l.Qty > 1)   // ⚠ the same rows, spelled the long way
```

Both are legal — the compiler does not stop you, and there is no correctness difference. But **prefer the
collection**, and the reason is one you will feel later rather than now:

- **You cannot get the join condition wrong** if you never write it. The `l.Order == order` in the second form is a
  condition you have to remember, on every query, forever; the first form has it built in.
- **It reads as what it is.** `order.Lines` is "this order's lines". The FK filter is a re-derivation of a fact the
  model already knows.
- **It is the shape the client's data layer understands.** A collection navigated from a parent stays coherent with
  the parent when it changes; a hand-rolled FK query is a detached result that does not.

Reach for the root query (`Line.Where(…)`) when you are genuinely asking a question **about all the children** —
"every backordered line across every order" — rather than about one parent's. That is a different question, and the
root query is the honest way to write it.

### How do I filter parents by a fact about their children?   {#in-a-predicate}
A collection used inside a `Where` on the *parent* lowers to a correlated subquery — which is how you filter parents
by a fact about their children:

```osy title="orders that contain a backordered line" test app=query-collections
List<Order> WithBackorder() {
  return Order.Where(o => o.Lines.Any(l => l.Qty == 0)).ToList();   // → WHERE EXISTS (…)
}

List<Order> Large() {
  return Order.Where(o => o.Lines.Count() > 10).ToList();           // → WHERE (SELECT COUNT(*) …) > 10
}
```

No lines are fetched by either. The database answers the question about the children while it is deciding which
parents to return.

## Examples       {#examples}
Iterating a parent's children, and the `Include` that makes doing it over many parents affordable:

```osy title="the loop, and the one word that makes it cheap" test app=query-collections
decimal InvoiceRun() {
  var orders = Order.Include(o => o.Lines).ToList();   // ← without this, one query PER order below

  decimal total = 0m;
  foreach (var o in orders) {
    foreach (var l in o.Lines) {
      total = total + l.Amount;
    }
  }
  return total;
}
```

## See also       {#see-also}
- [Include (pre-loading relations)](https://osysharp.com/reference/query/include/) — pre-loading children so a loop over parents is one query, not N+1
- [relations](https://osysharp.com/reference/entity/relations/) — declaring the relation (`[ForeignKey(...)]` and the collection it defines)
- [Sum / Average / Min / Max / Count](https://osysharp.com/reference/query/aggregates/) — `Sum`/`Count` over a collection, and what each answers over no rows
- [Querying data](https://osysharp.com/reference/query/index/) — the three things you can query, and what each costs


---

<!-- https://osysharp.com/reference/query/delete/ -->

# Delete

> Ends a query chain with a set-based DELETE: every row the chain selects is deleted in the database, immediately, in one statement — and the call answers how many went. Zero is an answer, not an error. A row the caller cannot read, or that the entity's delete rules refuse, is simply not in the set.

<!-- id: query-delete · area: query · stability: stable · html: https://osysharp.com/reference/query/delete/ -->

## Summary        {#summary}
`.Delete()` ends a query chain the way `.Count()` does — but instead of counting the matching rows it **deletes**
them, set-based, in the database, at the call site. The answer is how many rows were deleted. It is the same verb
as C#'s `ExecuteDelete`, under the natural name.

## Signature      {#signature}
```osy syntax
<Entity>.Where(o => …).Delete()                    →   int   // every matching row, one statement
<Entity>.Where(o => …).OrderBy(k).Take(n).Delete() →   int   // at most n rows — the chunked purge
<Entity>.Delete()                                  →   int   // the WHOLE set — deliberate, like C#'s ExecuteDelete
<a Query<T> binding or parameter>.Delete()         →   int   // a deferred chain composes into the terminal
```

## Description    {#description}

### Which rows does it delete?   {#the-target-set}
Exactly the rows the chain would have **returned to you**, narrowed further by the entity's `allow delete` rules.
A row your read security hides is not in the set; a row the delete rules refuse stays untouched. Neither is an
error — the statement deletes fewer rows, and the count says how many it was.

```osy title="delete all of a company's stale orders" test app=query-delete
entity Order {
  [Required] string Status;
  decimal Total;
}

int PurgeStale() {
  return Order.Where(o => o.Status == "Stale").Delete();
}
```

Zero is a normal answer: a predicate that matches nothing deletes nothing and answers `0`. A bare
`Order.Delete()` is the whole set, on purpose — the receiver names the set as plainly as `Order.Count()` does, and
the declared security still bounds it. And a [`Query<T>`](https://osysharp.com/reference/query/deferred/) built elsewhere — a binding, or a
parameter crossing a function boundary — ends in the terminal exactly like an inline chain.

### When does it run?   {#immediacy}
Immediately — at the call, not at `UnitOfWork.Commit()`. It is a statement against the stored rows, so it does not
see rows you have created or edited in the current unit of work. If you hold uncommitted changes of the same
entity type, the call refuses and tells you to `Commit()` or `Discard()` them first — silently deleting around
your pending edits would be worse.

### What happens to related rows?   {#cascade}
The same thing a per-row delete does: a required child (`[Required] Order Order;` on the child) is deleted with
its parent, an optional reference is set to null, and a relation declared to restrict blocks the delete. The
answered count is the **target** rows — cascaded children are not counted.

```osy title="children go with their parents; the count is the parents" test app=query-delete
entity Invoice {
  [Required] string State;
  [ForeignKey(Invoice)] InvoiceLine[] Lines;
}
entity InvoiceLine {
  [Required] Invoice Invoice;   // required → deleted with its invoice
  int Qty;
}

int DropDrafts() {
  return Invoice.Where(i => i.State == "Draft").Delete();   // lines cascade; count = invoices
}
```

### How do I delete a lot without one huge statement?   {#chunked-purge}
`OrderBy` and `Take` compose like on any chain, which gives the chunked-purge idiom — delete a bounded slice per
call and stop when the answer is zero:

```osy title="purge in bounded chunks" test app=query-delete
int PurgeOldest() {
  return Order.Where(o => o.Status == "Stale").OrderBy(o => o.Total).Take(100).Delete();
}
```

### One row I already hold?   {#per-row}
The receiver decides which verb you get. A **loaded entity** deletes that row through the unit of work, at commit,
like any other staged write; a **query chain** deletes set-based, immediately:

```osy title="the two receivers, side by side" test app=query-delete
void DropOne(Order o) {
  o.Delete();                                       // this row — staged, lands at commit
}
int DropMatching(string status) {
  return Order.Where(x => x.Status == status).Delete();   // the set — immediate, counted
}
```

### A list in memory?   {#local-lists}
`.Delete()` deletes **database rows**. A local list already has its verb — `list.RemoveAll(x => …)` — and the
compiler says so if you reach for the wrong one.

## Examples       {#examples}
```osy title="a maintenance function" test app=query-delete
entity Session2 {
  [Required] string Token;
  DateTime ExpiresAt;
}

int ReapExpired() {
  return Session2.Where(s => s.ExpiresAt < DateTime.UtcNow).Delete();
}
```

## See also       {#see-also}
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — saying which rows, before the terminal
- [Skip / Take (paging)](https://osysharp.com/reference/query/paging/) — `Take`, for the chunked purge
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `allow delete` rules that narrow the set


---

<!-- https://osysharp.com/reference/query/distinct/ -->

# Distinct

> Removes duplicate rows from a query result. On whole entity rows it is a no-op, because rows are already unique by Id — it earns its keep on projections, where duplicates are real.

<!-- id: query-distinct · area: query · stability: stable · html: https://osysharp.com/reference/query/distinct/ -->

## Summary        {#summary}
`.Distinct()` removes duplicates from a result. It takes no arguments — "distinct" means *the whole row*, exactly as
in SQL.

## Signature      {#signature}
```osy syntax
<query>.Distinct().ToList()
```

## Description    {#description}

### On whole rows it does nothing   {#no-op-on-rows}
Entity rows are already unique — each has its own `Id` — so `Order.Where(…).Distinct()` cannot remove anything. It is
harmless, and it is also pointless, and writing it usually means someone expected it to do something it does not:

```osy title="distinct over entity rows changes nothing" test app=query-distinct
entity Order {
  [Required] string Code;
  [MaxLength(100)] string Region;
  decimal Total;
}

Order[] All() {
  return Order.Where(o => o.Total > 0).Distinct().ToList();   // same rows, either way
}
```

If what you meant was "one order per region", that is not `Distinct` — it is a grouping, or a projection of the region
alone.

### When does `Distinct` actually remove something?   {#projections}
Duplicates are real the moment you stop selecting whole rows. Two orders from the same region project to the same
region string — and *that* is where `Distinct` does the work you wanted:

```osy title="the regions we have orders in" test app=query-distinct
int RegionCount() {
  return Order.Where(o => o.Total > 0).Distinct().ToList().Count;
}
```

### What does `Distinct` cost?   {#cost}
De-duplicating means the database must compare rows, which usually means sorting them. On a large result that is real
work. If you are reaching for `Distinct` to paper over a join that is producing duplicates, fix the join — the
duplicates are a symptom, and `Distinct` only hides it.

## See also       {#see-also}
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — the query being de-duplicated
- [ToList](https://osysharp.com/reference/query/tolist/) — materialising the result
- [Union / Concat / Intersect / Except](https://osysharp.com/reference/query/set-operators/) — union / intersect / except, which also concern duplicates


---

<!-- https://osysharp.com/reference/query/dynamic-in/ -->

# Dynamic IN (list.Contains in a query)

> Filter a query by membership in a RUNTIME list — list.Contains(e.Column) inside a Where lowers to SQL `= ANY(@param)`, passing the whole list as one array parameter. The list can be any local List<T> whose element type matches the column; an empty list matches nothing.

<!-- id: query-dynamic-in · area: query · stability: stable · html: https://osysharp.com/reference/query/dynamic-in/ -->

## Summary        {#summary}
Inside a query predicate, `list.Contains(e.Column)` tests each row's column for membership in a **runtime**
`List<T>` — exactly the C#/LINQ spelling for SQL `IN`. It lowers to `e.Column = ANY(@p)`, binding the whole
list as **one** array parameter (stable SQL shape). The list is a local you build at run time; an **empty**
list matches nothing.

## Signature      {#signature}
```osy syntax
Entity.Where(e => <list>.Contains(e.<Column>))    // → e.Column = ANY(@p)
```

## Description    {#description}
The receiver `<list>` is any local collection (a `new List<T>()` you populate, a `Text.Split` result, …)
whose element type is **comparable to the column** — same rule as a literal `IN` or `==`: exact for
`string`/`bool`/`DateTime`, id-coercion between `Guid` and `string`, and numeric widening
(`int`→`long`→`decimal`→`double`). An incompatible pair is a compile error.

This is **position-sensitive**: the same `list.Contains(x)` written **outside** a query predicate is the
ordinary in-memory list-membership check. It becomes a SQL `IN` only inside a `Where`/`Any`/`Count`/…
predicate, where `x` is a row column.

A literal list works too and is equivalent: `[a, b, c].Contains(e.Column)` (rendered as `IN (…)`); the
runtime-list form is the one that lets the set be computed at run time.

## Examples       {#examples}
```osy title="filter by a runtime id set" test app=dynamic-in
entity Ticket { string Status; }

List<Ticket> ByIds(List<Guid> ids) {
  return Ticket.Where(t => ids.Contains(t.Id)).ToList();   // t.Id = ANY(@p)
}

List<Ticket> ByStatuses() {
  var open = new List<string>();
  open.Add("New");
  open.Add("InProgress");
  return Ticket.Where(t => open.Contains(t.Status)).ToList();   // empty `open` → no rows
}
```

```osy title="on a page: one live var computes the set, the next reads the rows it selects" test app=dynamic-in-page
entity Tag {
  [Required, MaxLength(50)] string Name;
  bool Active;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

entity Item {
  [Required, MaxLength(50)] string Title;
  Tag Tag;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

[Page("/board")]
[AllowAnonymous]
[Render(CSR)]
component Board() {
  live var activeIds = Tag.Where(t => t.Active).Select(t => t.Id).ToList();
  live var work = Item.Where(i => activeIds.Contains(i.Tag.Id)).ToList();   // the set crosses the wire

  render {
    Stack {
      Text("in scope: " + work.Count);
      foreach (var w in work) { Text(w.Title); }
    }
  }
}
```

The captured list is sent to the server as one query input, so the filter runs in the database over the whole
table — not over rows the page had already fetched. And because it is an input, the second read **follows** the
first: when `activeIds` changes, `work` re-runs against the new set with nothing to wire up.

Both members are server reads, which `osy validate` will tell you:

```console
ⓘ reads the SERVER holds: Board.activeIds, Board.work — these re-run when their DATA changes.
```

## See also       {#see-also}
- [Union / Concat / Intersect / Except](https://osysharp.com/reference/query/set-operators/) — combining whole row-sets (Union/Intersect/Except)
- [List OrderBy (in-memory)](https://osysharp.com/reference/function/list-orderby/) — ranking a local list in memory
- [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) — what makes a `live var` re-run, and what a second one reading the first depends on


---

<!-- https://osysharp.com/reference/query/single-row/ -->

# First / Single / Last / ElementAt

> The terminals that return ONE row. They differ in what they promise, and choosing the wrong one is how a bug hides: `Single` asserts there is exactly one and faults if there are two; `First` takes the first of however many; the `…OrDefault` twins return null instead of faulting on empty. `Last` requires an `OrderBy` — "the last row" is meaningless without an order, and the database will not guess one.

<!-- id: query-single-row · area: query · stability: stable · html: https://osysharp.com/reference/query/single-row/ -->

## Summary        {#summary}
These return one row, and **the one you pick is an assertion about your data**:

```osy title="each one says something different" test app=query-single-row
entity Order {
  [Required, MaxLength(40), Unique] string Code;
  decimal Total;
  DateTime PlacedAt;
}

Order ByCode(string code) {
  return Order.Single(o => o.Code == code);        // Code is unique — say so, and be told if it is ever not
}

Order? MaybeByCode(string code) {
  return Order.SingleOrDefault(o => o.Code == code);   // …and it may legitimately not exist
}

Order? Biggest() {
  return Order.OrderByDescending(o => o.Total).FirstOrDefault();   // "the biggest" — of however many
}
```

## Signature      {#signature}
```osy syntax
<Query>.First()   / .First(o => p)             // the first row; FAULTS if there is none
<Query>.FirstOrDefault() / .FirstOrDefault(o => p)   // …or null

<Query>.Single()  / .Single(o => p)            // exactly one; FAULTS on none AND on several
<Query>.SingleOrDefault() / .SingleOrDefault(o => p) // …null on none; still faults on several

<Query>.Last()    / .LastOrDefault()           // requires an OrderBy
<Query>.ElementAt(n) / .ElementAtOrDefault(n)  // the n-th row; n must be a constant
```

## Description    {#description}

### `First`, `Single`, or the `…OrDefault` forms?   {#choosing}
They are not interchangeable, and the difference is what each one *claims*:

| Call | If there is no row | If there are several | What it asserts |
|---|---|---|---|
| `First` | **faults** | returns the first | "there is at least one" |
| `FirstOrDefault` | `null` | returns the first | "there may or may not be one" |
| `Single` | **faults** | **faults** | "there is exactly one" |
| `SingleOrDefault` | `null` | **faults** | "there is at most one" |

**Reach for `Single` when the data says one.** Looking up by a `[Unique]` code, or by id: if two ever came back, your
data is broken and you want to know immediately — at the query, with a clear fault, rather than three screens later
when the wrong one turns out to have been picked. `First` in that position would silently choose one and carry on,
and *that* is the bug that takes a day to find.

**Reach for `First` when several is normal** and you want the top one — which almost always means you have said what
"top" means with an [`OrderBy`](https://osysharp.com/reference/query/ordering/). `First()` on an unordered query returns an arbitrary row, and the
database is free to pick a different one tomorrow.

### `Last` needs an order   {#last}
`Last()` and `LastOrDefault()` **require an `OrderBy`** — the compiler refuses them without one. There is no "last"
row in a set; there is only a last row in a *sequence*, and a sequence is what an `OrderBy` makes. The engine inverts
your ordering and takes one row, so it costs the same as `First()`.

```osy title="the most recent order" test app=query-single-row
Order? MostRecent() {
  return Order.OrderBy(o => o.PlacedAt).LastOrDefault();   // …or OrderByDescending + FirstOrDefault. Same query.
}
```

### How do I take the n-th row? — `ElementAt`   {#element-at}
`ElementAt(n)` takes the n-th row (0-based) and needs a **compile-time constant** index — it becomes an `OFFSET`, and
it composes additively with `Skip`. For a *runtime* offset, that is what [`Skip`/`Take`](https://osysharp.com/reference/query/paging/) is for.

### Using one inside another query   {#inline}
A single-row read can be written **inline** where another query needs its value, and it means what the two-line
version means:

```osy title="inline and hoisted are the same query" test app=query-single-row
entity Invoice {
  [Required, MaxLength(40), Unique] string Number;
  [ForeignKey(Invoice)] InvoiceLine[] Lines;
}

entity InvoiceLine {
  [Required] Invoice Invoice;
  decimal Amount;
}

int LinesInline(string number) {
  return Invoice.Single(i => i.Number == number).Lines.Count();
}

int LinesHoisted(string number) {
  var invoice = Invoice.Single(i => i.Number == number);
  return invoice.Lines.Count();        // …what the line above is shorthand for
}
```

The inner read runs first and its result is used by the outer one — so `Single`'s promise still holds where you wrote
it (two matching invoices faults there, at the lookup, not somewhere downstream). Writing the local yourself is still
the clearer choice when you need the row again, or when the name says something.

This works where the read is a **value the outer query needs** — navigating into its collection, as above, or
comparing against it (`line.Invoice == Invoice.Single(…)`). Reading a FIELD straight off it
(`Invoice.Single(…).Number`) still wants the local.

And a read that **names the row being filtered** is a different read for every candidate row, so there is nothing to
run once. The compiler says so rather than guessing; reach the row you want through the reference or its collection
instead.

### None of them fetch more than they need   {#one-row}
Each of these becomes `LIMIT 1` (`Single` asks for two, so it can tell you when there are two). You are not fetching a
set and taking its head — the database returns one row.

You can [`Include`](https://osysharp.com/reference/query/include/) on any of them, which is exactly what a detail page wants: one parent, its
children already in hand.

## Examples       {#examples}
The three failure modes, said out loud:

```osy title="what each one does when the data disagrees with you" test app=query-single-row
Order Required(string code) {
  return Order.Single(o => o.Code == code);
  // no row  → faults (NotFound): the caller asked for a code that does not exist
  // two rows → faults (Conflict): `Code` is [Unique], so this is a broken database, not a lookup miss
}

Order? Optional(string code) {
  return Order.SingleOrDefault(o => o.Code == code);
  // no row  → null       (a legitimate "not found" — the caller decides what that means)
  // two rows → still faults (at most one is still an assertion, and it has been violated)
}

Order? Top() {
  return Order.OrderByDescending(o => o.Total).FirstOrDefault();
  // no row  → null; several → the biggest. Neither is an error: "several" is the normal case here.
}
```

## See also       {#see-also}
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — the predicate these take, and `Count`/`Any`
- [OrderBy / ThenBy](https://osysharp.com/reference/query/ordering/) — what `Last()` requires, and why `First()` without it is a coin toss
- [Include (pre-loading relations)](https://osysharp.com/reference/query/include/) — one row, with its relations pre-loaded
- [Skip / Take (paging)](https://osysharp.com/reference/query/paging/) — a runtime offset, which is what `ElementAt` is not


---

<!-- https://osysharp.com/reference/query/group-by/ -->

# GroupBy (and HAVING)

> Group rows by a key and reduce each group to one row — `g.Key`, `g.Count()`, `g.Sum/Average/Min/Max(…)` — computed by the database as a real GROUP BY. A `Where` BEFORE the grouping filters the rows that get grouped; a `Where` AFTER the projection filters the groups themselves, which is SQL's HAVING. You never write the word "having": which side of the `GroupBy` a `Where` sits on says what it filters.

<!-- id: query-group-by · area: query · stability: stable · html: https://osysharp.com/reference/query/group-by/ -->

## Summary        {#summary}
Group rows by a key and reduce each group to a single row, in the database:

```osy title="revenue per region, biggest first" test app=query-group-by
class RegionTotal {
  public string Region;
  public int Orders;
  public decimal Total;
}

entity Order {
  [Required, MaxLength(60)] string Region;
  decimal Total;
  bool Cancelled;
  int Month;
}

List<RegionTotal> Revenue() {
  return Order.Where(o => !o.Cancelled)                 // filters the ROWS, before grouping
              .GroupBy(o => o.Region)
              .Select(g => new RegionTotal {
                Region = g.Key,
                Orders = g.Count(),
                Total  = g.Sum(o => o.Total) })
              .Where(r => r.Total > 1000m)              // filters the GROUPS — this is HAVING
              .OrderByDescending(r => r.Total)
              .Take(10)
              .ToList();
}
```

That is one `SELECT … GROUP BY region HAVING SUM(total) > 1000 ORDER BY … LIMIT 10`. No rows travel.

## Signature      {#signature}
```osy syntax
<Entity>
  [.Where(o => rowPredicate)]              // optional — filters the ROWS that get grouped
  .GroupBy(o => key)                        // one key, or a composite: o => new() { A = o.X, B = o.Y }
  .Select(g => new T {                      // REQUIRED, and must come next
      Col = g.Key,                          //   the key (or g.Key.A for a composite)
      Agg = g.Sum(o => v),                  //   g.Count() · g.Sum/Average/Min/Max(o => v)
  })
  [.Where(x => groupPredicate)]             // optional — filters the GROUPS (HAVING)
  [.OrderBy(x => col) | .OrderByDescending(x => col)]
  [.Take(<constant int>)]
  [.ToList()]
```

## Description    {#description}

### Does my `Where` filter rows, or groups?   {#two-wheres}
The position of a `Where` is what it means, and this is the whole grammar of grouping:

| Where it sits | What it filters | SQL |
|---|---|---|
| **before** `GroupBy` | the **rows** that get grouped | `WHERE` |
| **after** the `Select` | the **groups**, by their aggregates | `HAVING` |

*Exclude cancelled orders from the totals* is a row filter. *Only show regions that made over £1000* is a group
filter — and it cannot be a row filter, because no single row knows its group's total. You never type the word
`having`; you put the `Where` on the side that says what you mean.

### Why must `GroupBy` be followed by `Select`?   {#projection}
**`GroupBy` must be followed immediately by `Select`.** There is no `IGrouping` value you can hold onto, hand around
or iterate — a group only exists as the row it reduces to. That is a deliberate limit: a group you could carry around
would be a promise to fetch its members later, which is the fetch this whole verb exists to avoid.

Inside that `Select`, a column is exactly one of two things:

- **the key** — `g.Key` (or `g.Key.<Part>` for a composite key), or
- **an aggregate** — `g.Count()`, `g.Sum(o => v)`, `g.Average(o => v)`, `g.Min(o => v)`, `g.Max(o => v)`.

Anything else is a compile error, because anything else would need a row that the group does not have.

**`g.Count()` takes no predicate.** `g.Count(o => p)` is not supported; filter before the group, or sum a condition.

### How do I group by more than one column?   {#composite-keys}
Group by more than one column with an object key, and read the parts back off `g.Key`:

```osy title="a breakdown by two columns" test app=query-group-by
class MonthlyRegion {
  public string Region;
  public int Month;
  public decimal Total;
}

List<MonthlyRegion> ByRegionAndMonth() {
  return Order.GroupBy(o => new() { Region = o.Region, Month = o.Month })
              .Select(g => new MonthlyRegion {
                Region = g.Key.Region,          // ← per PART, not a bare g.Key
                Month  = g.Key.Month,
                Total  = g.Sum(o => o.Total) })
              .OrderBy(r => r.Region)
              .ToList();
}
```

A bare `g.Key` on a composite key is a compile error — there is no tuple type to hand you, and each part is a column
in its own right.

### The projection target is a `class`   {#target}
Project into a [`class`](https://osysharp.com/reference/class/methods/) you declared (a plain data shape). That is what gives the result a real type
you can return, index and iterate — `List<RegionTotal>` above. An anonymous `new { … }` also works, but it has no
name, so it cannot cross a function boundary; use it only where the result is consumed on the spot.

### What may NOT precede the grouping   {#restrictions}
**Only `Where`.** No `OrderBy`, no `Skip`/`Take`, no `Include`, no `Distinct` before a `GroupBy` — and the compiler
says so rather than quietly ignoring them. Sorting rows that are about to be collapsed into groups would mean nothing;
sort the **groups** after the projection, which is exactly what is supported. Paging the rows before grouping would
compute your totals from an arbitrary slice of the table, which is a bug rather than a feature.

After the projection, `Take` needs a **compile-time constant** (`Take(10)`, not `Take(n)`) — the top-N of a grouped
report is a fixed shape, not a runtime page.

## Examples       {#examples}
Every aggregate at once, over a parent's children — a per-invoice summary computed entirely in the database:

```osy title="min, max, average and count, per group" test app=query-group-by
class SkuStat {
  public string Sku;
  public int Times;
  public int TotalQty;
  public decimal Cheapest;
  public decimal Dearest;
  public decimal Average;
}

entity Line {
  [Required, MaxLength(60)] string Sku;
  int Qty;
  decimal Price;
}

List<SkuStat> PerSku() {
  return Line.GroupBy(l => l.Sku)
             .Select(g => new SkuStat {
               Sku      = g.Key,
               Times    = g.Count(),
               TotalQty = g.Sum(l => l.Qty),
               Cheapest = g.Min(l => l.Price),
               Dearest  = g.Max(l => l.Price),
               Average  = g.Average(l => l.Price) })
             .Where(s => s.Times > 1)              // HAVING COUNT(*) > 1 — SKUs sold more than once
             .OrderByDescending(s => s.TotalQty)
             .ToList();
}
```

## See also       {#see-also}
- [Sum / Average / Min / Max / Count](https://osysharp.com/reference/query/aggregates/) — the same folds over a whole query rather than per group (and the null-on-empty rule)
- [Select (projections)](https://osysharp.com/reference/query/select/) — projections without grouping
- [OrderBy / ThenBy](https://osysharp.com/reference/query/ordering/) — sorting the groups by an aggregate
- [LINQ over a local list](https://osysharp.com/reference/query/in-memory-linq/) — `GroupBy` over a local `List<T>`, evaluated in memory
- [class methods](https://osysharp.com/reference/class/methods/) — the `class` a grouped projection targets


---

<!-- https://osysharp.com/reference/query/include/ -->

# Include (pre-loading relations)

> Pre-load the related rows a query's results are about to navigate to. `Include(o => o.Lines)` does not change what comes back — the same rows, the same type — it just means the children are already in hand, so the loop that walks them costs nothing instead of firing one query per parent. It is the fix for the N+1 problem, and the only reason you ever need to think about it.

<!-- id: query-include · area: query · stability: stable · html: https://osysharp.com/reference/query/include/ -->

## Summary        {#summary}
`Include` pre-loads the relations your results are about to walk:

```osy title="one query for the orders, one for all their lines" test app=query-include
entity Order {
  [Required, MaxLength(40)] string Code;
  [ForeignKey(Order)] Line[] Lines;
}

entity Line {
  [Required] Order Order;
  Product Product;                 // the reference the nested Include walks to
  [MaxLength(60)] string Sku;
  decimal Amount;
}

decimal TotalOfRecentOrders() {
  var orders = Order.OrderByDescending(o => o.Code)
                    .Take(50)
                    .Include(o => o.Lines)      // ← the lines come back with the orders
                    .ToList();

  decimal total = 0m;
  foreach (var o in orders) {
    foreach (var l in o.Lines) { total = total + l.Amount; }   // already in memory — no query in this loop
  }
  return total;
}
```

Delete the `Include` and that code still works — and quietly fires **one query per order**. That is the whole point of
the verb: **in a function body it is a *performance* declaration, not a semantic one.**

⚠ **On a page it is REQUIRED, not an optimisation.** The sentence above is true of code that runs on the server,
where a relation you did not pre-load is fetched on demand. A page renders on the client, which has no such
fallback: it can only walk what the query actually brought back. See [[query-include#on-a-page]] before you leave
one out.

## Signature      {#signature}
```osy syntax
<Query>.Include(o => o.Collection)          // a child collection
<Query>.Include(o => o.Reference)           // an entity reference
<Query>.Include(o => o.Lines.Product)       // NESTED — a dotted path in ONE lambda
<Query>.Include(a).Include(b)               // repeatable — they accumulate
```

A **member-selector lambda**, not a string. There is no `ThenInclude`: nesting is a dotted path inside the one lambda,
and every step of the path is loaded, not just the leaf.

## Description    {#description}

### It changes performance, not results   {#not-semantics}
An `Include` returns **exactly the same rows, of exactly the same type**, as the query without it. It adds nothing to
the `SELECT`, filters nothing, and never appears in the row shape. All it does is load the related rows into memory
*with* the parents, so the navigation you were going to do anyway is already paid for.

This is worth internalising, because it explains why you can add or remove one freely **in a function body**: an
`Include` there cannot change what your code computes — only how many round trips it takes to compute it. If adding
one changes an answer, the answer was wrong before.

**That freedom is the server's, and only the server's.** The next section is the other half.

### On a page, it IS semantic — and leaving it out is an error   {#on-a-page}

A page's render runs on the client against the rows the query returned. There is no lazy load out there: a relation
that was not included did not travel, so the reference holds its raw key rather than the row it names. Walking it
does not fetch anything and does not return empty — **it fails**, because you asked a key for a property only a row
has.

```osy syntax
// the query behind the page
var item = Item.Where(i => i.Code == code)
               .Include(i => i.Owner)              // ← REQUIRED: the render reads Owner.Name
               .Include(i => i.Links.Target)       // ← REQUIRED: and it walks THROUGH Links to each Target
               .FirstOrDefault();

render {
  Text(item.Owner.Name);                            // without the first Include: no row, no Name
  foreach (var l in item.Links) { Text(l.Target.Code); }   // without the SECOND: the links came, their targets did not
}
```

Two things follow, and both cost people time:

**A nested walk needs the nested path.** Including the collection is not enough — `Include(i => i.Links)` brings the
links and stops there, so `l.Target.Code` still has nothing to read. The dotted form loads every step.

**Do not conclude anything from a page that works without one.** Whether an un-included reference resolves depends
on whether *some other query on the same page* already loaded that row, because they share one store. So the same
render can be correct on a page that happens to list Owners elsewhere and fail on a page that does not — identical
code, different neighbours. Include what you walk, and the question never arises.

### The N+1 problem, which is the reason it exists   {#n-plus-one}
Fetch 50 orders, then loop over each one's lines. Without `Include`, each `o.Lines` is a *fresh query* — 1 query for
the orders and 50 for the lines. It is fast on your machine with 3 orders and it is an outage on a real database with
5,000. Nothing about the code looks wrong, which is what makes it worth a verb of its own.

With the `Include`, the children arrive with the parents, and the loop touches memory.

### How do I load two levels down?   {#nesting}
A dotted path in one lambda walks further down, loading every step:

```osy title="orders → their lines → each line's product" test app=query-include
entity Product {
  [Required, MaxLength(60)] string Name;
  decimal Price;
}

List<Order> WithEverything() {
  return Order.Include(o => o.Lines.Product)     // loads Lines AND each Line's Product
              .ToList();
}
```

There is no `ThenInclude` to chain — the path is the nesting. To load two *different* branches, call `Include` twice.

### Where it may appear   {#position}
`Include` composes with `Where`, `OrderBy`, `Skip`/`Take`, `ToList()` and the [single-row
terminals](https://osysharp.com/reference/query/single-row/) (an `Include` on a `First()` is perfectly sensible — one parent, its children in hand).

It is **refused** in three places, each for the same reason — there would be no entity rows for it to attach the
relations to:

- before a [`Select`](https://osysharp.com/reference/query/select/) projection — a projection does not return entity rows. Select what you need.
- before a [`GroupBy`](https://osysharp.com/reference/query/group-by/) — a group is not a row.
- with the [set operators](https://osysharp.com/reference/query/set-operators/) — apply it after materialising.

### On a list you already hold   {#on-a-list}
A local [list](https://osysharp.com/reference/query/in-memory-linq/) takes `Include` too, and for exactly the same reason a server query does:
holding the ROWS is not the same as holding what they REFER to. A row carries a reference as a value, so a
client-side `Where` that navigates one has nothing to read through unless it was included first.

```osy title="include a reference before filtering on it in memory" test app=query-include-in-memory
entity Supplier { [Required] [MaxLength(80)] string Name; security { allow read, create when IsAuthenticated || IsAnonymous; } }
entity Part {
  [Required] [MaxLength(80)] string Code;
  [Required] Supplier Supplier;
  security { allow read, create when IsAuthenticated || IsAnonymous; }
}

int AcmeParts() {
  var parts = Part.ToList();
  return parts.Include(p => p.Supplier).Where(p => p.Supplier.Name == "Acme").Count();
}
```

The load is **batched**: one pass per hop, over the distinct references in the whole list — not one read per row. A
deeper path (`p => p.Supplier.Region`) loads the second hop across everything the first hop returned, so the cost
does not grow with the number of rows.

Without the `Include`, navigating `p.Supplier.Name` in that filter is a compile error that names the fix — it does
not silently return blanks.

### In a component, it is added for you   {#auto}
A component's query fetches what its `render` reads. If the render navigates a reference the query did not include,
the compiler adds the `Include` rather than refusing the code:

```osy syntax
live var reports = Report.ToList();      // + .Include(Owner) — added, because the render below reads it

render {
  foreach (var r in reports) { Text(r.Owner.Email); }
}
```

Reading `r.Owner.Email` **is** the request for `Owner` — there is no program that wants the read and not the fetch —
so requiring a second statement of the same fact only creates something that can drift out of step with the first.
The two spellings behave identically: a reference navigated inside a client-side `Where` is the same demand as one
read in an element, and both are added.

This is also how you end up on the efficient path without having to know about it. What gets added is an **eager**
load — one query with a join, batched per hop as described above — so the default is the one that avoids
[[#n-plus-one|N+1]], not a fetch per row.

It is not invisible: the editor shows what was added as a hint after the query (`+ .Include(Owner)`), so what the
query costs is still readable at the point you are reading it.

**The refusal is still there when the compiler cannot satisfy the demand** — a read through a reference on something
that is not a component query field is still a compile error naming the fix. Dropping such a read silently is the one
outcome worse than refusing it: the page would render a blank where the value should be.

### With a projection `Select`, there is nothing left to pre-load   {#with-a-projection}

`Include` and a projection `Select` do not combine, in either order, and the refusal says so. It is not a limitation
of projections — it is that the two ask for the same thing. A projection decides what comes back, so the eager load
has nothing left to pre-load. (C# on a database behaves the same way: an `Include` is ignored once the query ends in
a projection.)

**A projection reaches through a reference on its own**, which is the part worth knowing — it needs no `Include`
anywhere:

```osy title="a column of the related row — no Include" test app=include-projection
entity CostCenter {
  [Required, MaxLength(80)] string Name;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

entity ExpenseLine {
  [Required, MaxLength(80)] string Description;
  decimal Amount;
  CostCenter CostCenter;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

List<string> CentreNames() {
  return ExpenseLine.Where(l => l.Amount > 0)
                    .Select(l => l.CostCenter.Name)     // reaches through the reference — no Include
                    .ToList();
}
```

So there are two shapes, and the one you want depends on whether you need a **column** or the **row**:

| you want | write |
|---|---|
| a column of the related row | `.Select(l => l.CostCenter.Name)` — no `Include` |
| the related row itself | `.Include(l => l.CostCenter).ToList()` — no `Select` |

Adding `.Include(…)` to the first is refused rather than quietly dropped, deliberately: `Include` is the fix for
N+1 and nothing else, so accepting and ignoring it would tell you that you had solved a cost problem you had not
touched.

## Examples       {#examples}
Include on a single-row terminal — the shape a detail page uses:

```osy title="one order, with its lines already loaded" test app=query-include
Order? Detail(string code) {
  return Order.Where(o => o.Code == code)
              .Include(o => o.Lines.Product)
              .FirstOrDefault();
}
```

## See also       {#see-also}
- [Child collections (navigating a relation)](https://osysharp.com/reference/query/collections/) — the navigation `Include` makes free (and what a child collection actually is)
- [relations](https://osysharp.com/reference/entity/relations/) — declaring the relation in the first place
- [Select (projections)](https://osysharp.com/reference/query/select/) — the other answer to "I only need part of this": ask for fewer columns
- [Querying data](https://osysharp.com/reference/query/index/) — the shape of a query chain


---

<!-- https://osysharp.com/reference/query/insert-from/ -->

# Insert

> Ends a query chain by creating one row of ANOTHER entity per row the chain selects — an INSERT … SELECT in one statement. The chain is the source; the `new T { … }` inside the terminal names the target and its properties, and each value may read the source row. Returns how many rows were created.

<!-- id: query-insert-from · area: query · stability: stable · html: https://osysharp.com/reference/query/insert-from/ -->

## Summary        {#summary}
`.Insert(…)` closes the other load-and-loop pattern — *create a row per matching parent*. The chain selects the
SOURCE rows; the projection inside the terminal builds one TARGET row from each, in the database, in one
statement. The answer is how many rows were created.

## Signature      {#signature}
```osy syntax
<Source>.Where(s => …).Insert(s => new <Target> { Property = s.Property, Other = literal, Ref = s })   →   int
<list>.Insert(x => new <Target> { Property = x.Property, … })                                            →   int
```

## Description    {#description}

### One row per matching row   {#the-shape}
The canonical use grants something to everyone who lacks it — the **not-exists idiom**, which also makes the
statement re-runnable (already-covered rows simply do not match):

```osy title="a grant for every active member that has none" test app=query-insert-from
entity Member {
  [Required] string Email;
  bool Active;
}
entity Grant {
  [Required] Member Grantee;
  [Required] string Level;
}

int GrantAll() {
  return Member.Where(m => m.Active && !Grant.Any(g => g.Grantee == m))
               .Insert(m => new Grant { Grantee = m, Level = "Member" });
}
```

`Grantee = m` assigns the source ROW to a reference — each created row points at its own source. Any source
property can feed a target property (`Label = m.Email`), values may be captured locals or literals, may read
through the source row's references, and may embed a correlated scalar read — the same value rules as
[`.Update(…)`](https://osysharp.com/reference/query/update/), including the answer-for-absence refusal on a hop through a nullable reference.
Only a **row-returning** query is refused: a projection assigns one scalar per column.

### What must the projection set?   {#required}
Every **required** target property, and every property whose default is a per-row **expression** — and the compiler
says so, naming the field, before anything runs. That is required-by-default arriving EARLIER than it does for a
per-row `new`, because the projection is statically known. A property with a literal default (`string Status =
"New";`) may be omitted and gets its default, exactly as a per-row create would give it.

### Which rows does it read, and may it create?   {#security}
The source chain is the caller's ordinary secured read. The target's `allow create when` is a caller-level gate,
checked once — a caller who may not create these rows gets a refusal, not a smaller set. An `allow create where`
(the with-check on the written row) is verified over the created rows inside the same transaction: one violating
row rolls the whole statement back.

### What about collisions?   {#unique}
Two answers, and both are good ones. Without more, a `[Unique]` collision throws for the whole statement —
all-or-nothing, carrying the message your `[Unique]` declares — and the not-exists predicate above avoids minting
the duplicate at all. Or say what a collision MEANS with `onConflict:` — the **upsert**:

```osy title="insert new rows, merge colliding ones — safely re-runnable" test app=query-insert-from
entity Staged { [Required] string Sku; int Qty; bool Ready; }
entity Product {
  [Unique] string Sku;
  decimal Price;
  int Stock;
}

int Sync() =>
  Staged.Where(s => s.Ready)
        .Insert(s => new Product { Sku = s.Sku, Price = 10m, Stock = s.Qty },
                onConflict: (p, inc) => {
                  p.Price = inc.Price;              // take the incoming value
                  p.Stock = p.Stock + inc.Stock;    // or combine with what is already there
                });
```

`p` is the EXISTING row; `inc` is the row that **would have been inserted**, carrying the projected values — the
only shape a conflict can see. The collision key is inferred from the target's own `[Unique]` declaration (stated
once, at the model), the merge may not move the row off its key, and the merge half is judged as the UPDATE it is —
your `allow update` rules, per assigned property. Merged rows are audited as updates, inserted ones as creates.

### From a local list   {#from-a-list}
The source need not be stored. A list or array built in the body — of class instances, or of plain scalars — is a
source too, with the SAME verb, the same required-field checks at compile, the same stamps, defaults, security and
audit per row, and the same `onConflict:`. The projection runs per element in your function; the rows go to the
database as one statement (a very long list goes in several, inside one transaction — a failure anywhere leaves
nothing). This is the seeding shape, and the import shape:

```osy title="seed rows from a list — one statement, not one create per element" test app=query-insert-from
class Draft { public string Name; public int Weight; }
entity Tag { [Required] string Name; int Weight; string Status = "New"; }

int Seed() {
  var drafts = new List<Draft> {
    new Draft { Name = "alpha", Weight = 1 },
    new Draft { Name = "beta", Weight = 2 },
  };
  return drafts.Insert(d => new Tag { Name = d.Name, Weight = d.Weight * 10 });
}
```

A scalar list is the same thing with the element itself as the value:

```osy title="a name per element, merging any that already exist" test app=query-insert-from
entity Label { [Unique] string Name; int Seen; }

int Mark(List<string> names) =>
  names.Insert(n => new Label { Name = n, Seen = 1 },
               onConflict: (l, inc) => { l.Seen = l.Seen + 1; });
```

### When does it run?   {#immediacy}
Immediately, at the call — like its two siblings, and with the same refusal while your unit of work holds
uncommitted changes of the SOURCE type.

## Examples       {#examples}
```osy title="one audit-shaped row per closed order" test app=query-insert-from
entity Order {
  [Required] string Status;
  decimal Total;
}
entity Settlement {
  [Required] string Kind;
  decimal Amount;
}

int Settle() {
  return Order.Where(o => o.Status == "Closed")
              .Insert(o => new Settlement { Kind = "order", Amount = o.Total });
}
```

## See also       {#see-also}
- [Update](https://osysharp.com/reference/query/update/) · [Delete](https://osysharp.com/reference/query/delete/) — the other two bulk terminals, same security story
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — selecting the source set
- [security { }](https://osysharp.com/reference/security/entity-security/) — `allow create when` / `allow create where`


---

<!-- https://osysharp.com/reference/query/joins/ -->

# Join / LeftJoin / SelectMany

> Combine two entities into one result. `Join` keeps the rows that match on both sides; `LeftJoin` keeps every row on the left and gives you null on the right where there is no match; `SelectMany` flattens a parent and its children into one row per child. Most of the time you do NOT need any of them — a relation you declared is navigated, not joined — so reach for these when the two things are related by a VALUE rather than by a reference.

<!-- id: query-joins · area: query · stability: stable · html: https://osysharp.com/reference/query/joins/ -->

## Summary        {#summary}
Combine two entities into one row shape:

```osy title="an inner join on a value" test app=query-joins
class Row {
  public string OrderCode;
  public string CustomerName;
}

entity Customer {
  [Required, MaxLength(60)] string Ref;
  [MaxLength(120)] string Name;
}

entity Order {
  [Required, MaxLength(40)] string Code;
  [MaxLength(60)] string CustomerRef;      // related by a VALUE, not by a reference
  [ForeignKey(Order)] Line[] Lines;
}

List<Row> Rows() {
  return Order.Join(Customer,
                    o => o.CustomerRef,     // the key on the left
                    c => c.Ref,             // the key on the right
                    (o, c) => new Row { OrderCode = o.Code, CustomerName = c.Name })
            .ToList();
}
```

## Signature      {#signature}
```osy syntax
<Entity>.Join(<Other>, a => aKey, b => bKey, (a, b) => projection)       // INNER JOIN — matches on both sides
<Entity>.LeftJoin(<Other>, a => aKey, b => bKey, (a, b) => projection)   // LEFT JOIN — every left row; null on the right
<Entity>.SelectMany(a => a.Children, (a, c) => projection)               // flatten: one row per child
<Entity>.SelectMany(a => <Other>, (a, b) => projection)                  // every pairing (a cross join)
```

The range-variable names must be **the same** in the key selectors and the result selector — `o` and `c` above. That
is not a style rule; the compiler binds them by name.

## Description    {#description}

### First: you usually do not need a join   {#usually-not}
This is the most useful thing on the page. If the two entities are related by a **declared relation**, you do not join
them — you *navigate*:

```osy syntax
order.Customer.Name                          // a reference: just read it
order.Lines.Sum(l => l.Amount)               // a collection: query it ([Child collections (navigating a relation)](https://osysharp.com/reference/query/collections/))
Order.Include(o => o.Lines.Product).ToList() // pre-load a whole graph ([Include (pre-loading relations)](https://osysharp.com/reference/query/include/))
```

The relation already knows how the rows connect. Writing the join condition again by hand is a re-derivation of a fact
the model holds — and a chance to get it wrong.

**Reach for a join when there is no relation to navigate**: two entities related by a shared *value* (a code, a
reference, a slug) rather than by a foreign key. That is what the example above is — `CustomerRef` is a string that
happens to match `Customer.Ref`, and no relation ties them.

### `Join` vs `LeftJoin`   {#inner-vs-left}
- **`Join`** keeps only the rows that match on **both** sides. An order whose `CustomerRef` matches no customer simply
  is not in the result — which is sometimes exactly right, and sometimes how a row silently disappears from a report.
- **`LeftJoin`** keeps **every** row on the left, and hands you `null` on the right where nothing matched. Reach for it
  when the left side is the thing you are reporting on and the right side is extra detail.

```osy title="every order, even the ones with no matching customer" test app=query-joins
class Report {
  public string OrderCode;
  public string? CustomerName;   // null when nothing matched — that is the point
}

List<Report> AllOrders() {
  return Order.LeftJoin(Customer,
                        o => o.CustomerRef,
                        c => c.Ref,
                        (o, c) => new Report { OrderCode = o.Code, CustomerName = c.Name })
              .ToList();
}
```

If a report is missing rows you know exist, an inner join is the first thing to suspect.

### How do I get one row per child? — `SelectMany`   {#selectmany}
`SelectMany` turns a parent and its children into **one row per child**, which is the shape a flat export or a line
-level report wants:

```osy title="one row per line, carrying its order's code" test app=query-joins
class LineRow {
  public string OrderCode;
  public string Sku;
  public decimal Amount;
}

entity Line {
  [Required] Order Order;
  [MaxLength(60)] string Sku;
  decimal Amount;
}

List<LineRow> Flat() {
  return Order.SelectMany(o => o.Lines,
                          (o, l) => new LineRow { OrderCode = o.Code, Sku = l.Sku, Amount = l.Amount })
               .ToList();
}
```

Note what it is *not*: this does not fetch orders and then their lines. It is one statement — a join on the child's
foreign key — returning line-shaped rows.

Given an unrelated entity instead of a collection, `SelectMany` produces **every pairing** of the two (a cross join).
That is occasionally what you want and much more often a mistake; be sure.

### What does a join hand back?   {#projection}
Each of these takes a **result selector**, so a join always ends in a shape you named — usually a
[`class`](https://osysharp.com/reference/query/select/). There is no "joined entity" type to hand back: you say what the combined row looks like, and
that is what you get. A trailing `Where` / `OrderBy` / `Take` / `Skip` may follow, and the chain must end in that
projection.

These are **entity-only**. There is no join over a local [list](https://osysharp.com/reference/query/in-memory-linq/) yet.

## Examples       {#examples}
See the fences above — an inner join on a value, the left join that keeps the unmatched rows, and `SelectMany` for a
line-level flatten.

## See also       {#see-also}
- [Child collections (navigating a relation)](https://osysharp.com/reference/query/collections/) — navigating a declared relation, which is what you want most of the time
- [Include (pre-loading relations)](https://osysharp.com/reference/query/include/) — pre-loading a graph instead of flattening it
- [Select (projections)](https://osysharp.com/reference/query/select/) — the projection a join ends in
- [relations](https://osysharp.com/reference/entity/relations/) — declaring the relation that removes the need for a join


---

<!-- https://osysharp.com/reference/query/in-memory-linq/ -->

# LINQ over a local list

> Query a local `List<T>`, `HashSet<T>` or `T[]` — of your own `class` values OR of plain scalars like `string[]` and `int[]` — with the same LINQ verbs you use over data: `Where`, `Select` projections, `GroupBy` with per-group aggregates, `First`/`FirstOrDefault`/`Single`, `Any`, `Count`, `Sum`/`Average`/`Min`/`Max`, `Distinct`, `Skip`, `OrderBy`, `Take` — evaluated in memory. The data-store-only operations (full-text, similarity) don't apply to a plain list.

<!-- id: query-in-memory-linq · area: query · stability: stable · html: https://osysharp.com/reference/query/in-memory-linq/ -->

## Summary        {#summary}
The LINQ verbs work over a local **`List<T>`** — where `T` is a [`class`](https://osysharp.com/reference/class/methods/) *or* a plain scalar like
`string` or `int` — not just over your entities. The same spellings, evaluated in memory:

```osy title="filter, sort, total — over a list you built in code" test app=query-in-memory-linq
class Line { public string Sku; public int Amount; }

int TopThreeTotal(List<Line> lines) {
  var top = lines.Where(l => l.Amount > 0)
                 .OrderByDescending(l => l.Amount)
                 .Take(3);
  return top.Sum(l => l.Amount);
}
```

## Signature      {#signature}
```osy syntax
List<T> list.Where(x => predicate)                    // filter
List<U> list.Select(x => new U { … })                 // project into a class…
List<V> list.Select(x => x.Field)                     // …or into a scalar (→ List<string>, List<int>, …)
List<U> list.GroupBy(x => key).Select(g => new U { …g.Key, g.Count(), g.Sum(e => e.V)… })
T       list.First(x => p) / .Single(x => p)          // one element (First/Single fault when empty; …OrDefault → null)
bool    list.Any(x => p)                              // any match?
bool    list.All(x => p)                              // do they ALL match? (true for an empty list, as in C#)
int     list.Count() / .Count(x => p)                 // how many
V       list.Sum / Average / Min / Max(x => v)        // aggregate a selected value
List<T> list.OrderBy / OrderByDescending(x => k)      // sort
List<T> list.Skip(n) / .Take(n) / .Distinct()         // page / dedupe
List<T> list.Union / Concat / Intersect / Except(other)  // combine two lists (set operators)
List<T> list.ToList()                                 // a real, indexable List<T>
int     list.IndexOf(item)                            // WHERE is this element? -1 when it is not there
int     list.FindIndex(x => p)                        // …and where is the first one that MATCHES? -1 for none
```

The source may be a **`List<T>`, a `HashSet<T>`, an array (`T[]`), or any collection you already hold** — including
the rows a query returned; either way a collection result is a real, indexable `List<T>`. `T` may be a `class` or a
scalar.

## Description    {#description}
Given a `List<T>` of `class` values, the query verbs behave exactly as in C#: `Where` filters, `First`/`Single`
select one element (the `…OrDefault` forms return null instead of faulting on empty; `Single` faults when more than
one matches), `Any`/`All`/`Count` answer set questions, and `Sum`/`Average`/`Min`/`Max` fold a selected value — `Sum` and
`Min`/`Max` keep the selected value's type, `Average` is decimal. `Skip`/`Take` page; `Distinct` de-duplicates. A
collection result (`Where(…)`, `ToList()`) is a real `List<T>` — indexable, `.Count`, `foreach`. Predicates and
selectors may capture local variables.

The verbs **compose** in any order, matching C# LINQ:

```osy title="compose in any order" test app=query-in-memory-linq
List<Line> Page(List<Line> items) {
  return items.Where(i => i.Amount > 0).OrderBy(i => i.Sku).Skip(20).Take(10);
}
```

Find one element, with a safe fallback:

```osy title="the miss is a null, not a fault" test app=query-in-memory-linq
string SkuOrNone(List<Line> lines, int amount) {
  var hit = lines.FirstOrDefault(l => l.Amount == amount);   // null when there is none
  if (hit == null) { return "none"; }
  return hit.Sku;
}
```

### How do I reshape each element? — `Select`   {#select}
`Select` reshapes each element. Project into a **`class` you declared** (the C# spelling, `new U { … }`), or into a
**scalar** — which gives you a plain `List<string>` / `List<int>` of the selected values:

```osy title="project into a class, or into a scalar" test app=query-in-memory-linq
class LineDto { public string Code; public int Doubled; }

List<LineDto> ToDtos(List<Line> lines) {
  return lines.Select(x => new LineDto { Code = x.Sku, Doubled = x.Amount * 2 });
}

List<string> SkusByAmount(List<Line> lines) {
  return lines.OrderByDescending(l => l.Amount).Select(l => l.Sku);   // → a List<string>
}

int DistinctAmounts(List<Line> lines) {
  return lines.Select(l => l.Amount).Distinct().Count();              // dedupe the projected values
}
```

A **`HashSet<T>`** is a source too — filter and project a set exactly as you would a list:

```osy title="a HashSet is a LINQ source as well" test app=query-in-memory-linq
int BigOnesInSet(HashSet<Line> set) {
  return set.Where(x => x.Amount >= 20).Count();
}
```

### Can I query a `string[]` or an `int[]`?   {#scalar-elements}
`T` does not have to be a `class`. A `string[]`, an `int[]`, a `List<decimal>` — any sequence of scalars answers the
same verbs, and the range variable is simply the value:

```osy title="the same verbs, over plain values" test app=query-in-memory-linq
int NamesStartingWithA(string[] names) {
  return names.Where(n => n.StartsWith("a")).Count();
}

bool AllNamed(string[] names) {
  return names.All(n => n != "");        // true when there are no names at all, exactly as in C#
}

string FirstAlphabetically(string[] names) {
  return names.OrderBy(k => k).First(n => n != "");
}

List<int> Lengths(string[] names) {
  return names.Select(n => n.Length);        // → a List<int>
}

int TotalAndLargest(int[] amounts) {
  return amounts.Sum(a => a) + amounts.Max(a => a);
}
```

There is no separate scalar dialect to learn: `Where`, `Select`, `OrderBy`, `GroupBy`, `Any`/`All`, the aggregates,
the set operators and the `First`/`Single` family all read a scalar element exactly as they read a row. The one verb that
cannot apply is `Include` — it loads a *related* row, and a `string` has no relations, so it is refused by name.

### Rows you already fetched are a source too   {#fetched-rows}
Once a query has returned, its rows are just a collection you hold — so a second question about them is asked the same
way, with no "copy it into a list first" step:

```osy title="ask a second question about rows you already have" test app=query-in-memory-linq
bool AnyLarge(List<Line> lines) {
  var positive = lines.Where(l => l.Amount > 0).ToList();
  return positive.Any(l => l.Amount >= 100);   // over the rows already in hand — no second fetch
}
```

This is the same rule as everywhere else on this page — **what** you query decides where it runs. The first chain
above touched a list you built; had it named an entity, that chain would have run in the database, and only the rows
it returned would be here to ask about. The [`live var`](https://osysharp.com/reference/ui/reactivity/) a screen binds behaves identically: it holds
rows, so `.Count`, `.Any(…)`, `.Where(…)`, indexing and `foreach` all read it directly.

### Where in the list is it? — `IndexOf` and `FindIndex`   {#positions}
The LINQ verbs above answer *which* element; these two answer **where it sits**. Both are C#'s own, both count from
zero, and both answer **`-1`** when there is no match — never a fault, so `>= 0` is the guard you write:

```osy title="the position of an element, by value and by predicate" test app=query-in-memory-linq
int WhereIsIt(List<Line> lines, Line one) {
  return lines.IndexOf(one);                       // you HOLD the element — compare it
}

int WhereIsTheSku(List<Line> lines, string sku) {
  return lines.FindIndex(l => l.Sku == sku);       // you can DESCRIBE it — run a predicate
}

bool IsFirst(List<Line> lines, string sku) {
  return lines.FindIndex(l => l.Sku == sku) == 0;  // a miss is -1, so this is false rather than a fault
}
```

Which one you reach for is decided by what you are holding, and nothing else. **`IndexOf(item)`** takes the element
itself; **`FindIndex(x => …)`** takes a predicate, which is what you want whenever the thing you are looking for is
described rather than held — a value typed by a user, an id off the URL, a name off a form.

The two agree by construction: `IndexOf` compares elements the same way `==` does, so a row found by one is found by
the other at the same position.

⚠ **A list of ENTITY ROWS is not a special case, and this is the one people talk themselves out of.** `==` on two
entity references compares them by [row identity](https://osysharp.com/reference/entity/equality/), so `IndexOf(row)` finds the row **even when the
list came from one query and the row from another** — the two references are the same row. Writing
`FindIndex(x => x.Id == row.Id)` for that is not wrong, it is the same answer one lambda longer.

A position is what a **reorder** is written in terms of, and that is the commonest use — where a row is, what is
above it, whether it can move:

```osy title="the row above this one" test app=query-in-memory-linq
Line Previous(List<Line> ordered, string sku) {
  var at = ordered.FindIndex(l => l.Sku == sku);
  if (at <= 0) { return null; }                    // not found (-1), or already first
  return ordered[at - 1];
}
```

`FindIndex` searches the list **in order** and stops at the first match, so a predicate that matches twice answers the
earlier position.

### How do I total per group, and then filter the groups?   {#groupby}
`GroupBy(x => key)` buckets the elements by a key; the `Select` that follows reduces each bucket, reading the key as
**`g.Key`** and folding the group's elements with `g.Count()` / `g.Sum(…)` / `g.Min(…)` / `g.Max(…)` /
`g.Average(…)`. Project each group into a `class`:

```osy title="group by a key, reduce each group" test app=query-in-memory-linq
class GroupStat { public string Key; public int Count; public int Total; public int MaxAmount; }

List<GroupStat> PerSku(List<Line> lines) {
  return lines.GroupBy(x => x.Sku)
              .Select(g => new GroupStat {
                Key       = g.Key,
                Count     = g.Count(),
                Total     = g.Sum(e => e.Amount),
                MaxAmount = g.Max(e => e.Amount) })
              .OrderBy(s => s.Key);
}
```

A `Where` **before** the `GroupBy` filters the *elements* that get grouped. A `Where` **after** the projection filters
the *groups* — it is SQL's `HAVING`, written as an ordinary predicate over the projected shape, and it composes with
`OrderBy` and `Take` like anything else:

```osy title="filter elements, then filter groups (HAVING), then take the top" test app=query-in-memory-linq
List<GroupStat> TopSku(List<Line> lines) {
  return lines.Where(x => x.Amount >= 10)                  // filters ELEMENTS, before grouping
              .GroupBy(x => x.Sku)
              .Select(g => new GroupStat {
                Key       = g.Key,
                Count     = g.Count(),
                Total     = g.Sum(e => e.Amount),
                MaxAmount = g.Max(e => e.Amount) })
              .Where(s => s.Total >= 35)                   // filters GROUPS — this is HAVING
              .OrderByDescending(s => s.Total)
              .Take(1);
}
```

You never write the word `having`: which side of the `GroupBy` a `Where` sits on says what it filters, and that is the
whole rule.

### Set operators — combine two lists   {#set-operators}
`Union`, `Concat`, `Intersect`, and `Except` combine two local lists of the same `class`, exactly as in C#: `Union` is
the distinct elements of both, `Concat` keeps every element (duplicates and all), `Intersect` keeps the elements in
both, and `Except` keeps the left elements that are not in the right. The operand can be a plain list or its own filtered
chain:

```osy title="combine two lists" test app=query-in-memory-linq
List<Line> Both(List<Line> a, List<Line> b) {
  return a.Union(b.Where(l => l.Amount > 0)).ToList();
}
```

De-duplication (`Union`/`Intersect`/`Except`) uses the same equality as `Distinct`: a `class` value is compared by
**identity** (the same instance), a scalar by value — the C# default. Two lists that share an instance de-dupe it;
distinct instances stay distinct.

### Where does this run, and what is refused in memory?   {#in-memory-vs-data}
The distinction matters, and it is decided by **what you query**, not by which verb you use. Naming an entity
(`Order.Where(…)`) compiles the predicate **into the database query**, so the rows you did not ask for are never
fetched — and [the security rules are part of that query](https://osysharp.com/reference/security/entity-security/). Querying a `List<T>` you built
in code evaluates the predicate **over the elements you already have**, in the running function. Same spelling, and
that is the point: you do not learn two query languages. But a list of ten million elements is ten million elements
in memory, and no security rule applies to values you constructed yourself.

**What is not available on a local list.** The data-store-only operations are meaningless over values you already hold,
so they report a pointed error rather than pretending: full-text search (`Matches`/`TextScore`), vector `Similarity`,
`Traverse`, and `Include`. Use those over your entities, where they lower to the database. (`Include` is refused over a
scalar element for a second reason as well: a `string` has nothing related to load.)

Still coming in memory — each with its own "not supported yet" diagnostic, never a silent wrong answer — are
`SelectMany` and `Join`/`LeftJoin`. The set operators (`Union`/`Concat`/`Intersect`/`Except`) work over local lists as
well as [over entities](https://osysharp.com/reference/query/set-operators/).

## See also       {#see-also}
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — the same verbs over your entities, where they lower to the database
- [Skip / Take (paging)](https://osysharp.com/reference/query/paging/) — `Skip`/`Take` over your entities (the database form)
- [Union / Concat / Intersect / Except](https://osysharp.com/reference/query/set-operators/) — `Union`/`Concat`/`Intersect`/`Except` (the entity-query form; the spellings are identical)
- [Comparing entity rows](https://osysharp.com/reference/entity/equality/) — why `IndexOf`/`Contains`/`Remove` identify an entity row correctly, whatever query it came from
- [class methods](https://osysharp.com/reference/class/methods/) — the `class` types these lists hold


---

<!-- https://osysharp.com/reference/query/ordering/ -->

# OrderBy / ThenBy

> Sort a query by one key or several. `OrderBy`/`OrderByDescending` start the sort, `ThenBy`/`ThenByDescending` add further keys — and the keys accumulate into ONE composite sort, so a chained `OrderBy` behaves exactly like a `ThenBy` rather than re-sorting as it would in C#. It lowers to SQL `ORDER BY`, and it is what makes paging deterministic and `Last()` meaningful.

<!-- id: query-ordering · area: query · stability: stable · html: https://osysharp.com/reference/query/ordering/ -->

## Summary        {#summary}
Sort a query with `OrderBy`, and add further keys with `ThenBy`:

```osy title="newest first, ties broken by code" test app=query-ordering
entity Order {
  [Required, MaxLength(40)] string Code;
  decimal Total;
  DateTime PlacedAt;
}

List<Order> Newest(int howMany) {
  return Order.OrderByDescending(o => o.PlacedAt)
              .ThenBy(o => o.Code)          // a stable tiebreak — see below
              .Take(howMany)
              .ToList();
}
```

It becomes SQL `ORDER BY`. The database sorts; you do not fetch rows and sort them yourself.

## Signature      {#signature}
```osy syntax
<Query>.OrderBy(o => key)              // ascending
<Query>.OrderByDescending(o => key)    // descending
<Query>.ThenBy(o => key)               // a further key
<Query>.ThenByDescending(o => key)     // a further key, descending
```

Each takes a **key-selector lambda** with one parameter, and nothing else — there is no comparer overload and no
argument-less form. Any number of keys may accumulate.

## Description    {#description}

### The keys accumulate — a chained `OrderBy` does not re-sort   {#accumulate}
This is the one deliberate divergence from C#, and it is worth knowing before it surprises you:

```osy syntax
Order.OrderBy(o => o.Region).OrderBy(o => o.Total)     // sorts by (Region, Total)   ← NOT C# semantics
Order.OrderBy(o => o.Region).ThenBy(o => o.Total)      // sorts by (Region, Total)   ← the same thing
```

In C#, the second `OrderBy` would **replace** the sort — the result would be ordered by `Total` alone. Here the keys
build one composite sort, so the two lines above are identical. `ThenBy` is simply the spelling that says what is
happening, and it is the one to write.

### Sort before you page   {#page}
Without an `ORDER BY`, a database may return rows in any order it likes — and it may return them in a *different*
order for page 2 than it did for page 1. A paged query with no sort silently duplicates and drops rows.

```osy title="a page you can trust" test app=query-ordering
List<Order> Page(int page, int size) {
  return Order.OrderByDescending(o => o.PlacedAt)
              .ThenBy(o => o.Code)          // the tiebreak is what makes the page STABLE
              .Skip(page * size)
              .Take(size)
              .ToList();
}
```

And note the tiebreak. Sorting by a key with duplicates (many orders placed the same second) leaves their relative
order undefined, so a row can appear on two pages or on none. **Add a unique final key** — the `Code` above — and the
sort is total, so the pages partition the rows exactly. See [Skip / Take (paging)](https://osysharp.com/reference/query/paging/).

### `Last` needs an `OrderBy`   {#last}
`Last()` / `LastOrDefault()` **require** an `OrderBy` — "the last row" has no meaning without an order, and the
database will not guess one for you. The engine inverts your keys and takes one row, so it is as cheap as `First()`.
See [First / Single / Last / ElementAt](https://osysharp.com/reference/query/single-row/).

### Where it may appear   {#position}
`OrderBy` composes with `Where`, `Skip`/`Take`, `Include` and the terminals. Two restrictions are worth knowing, both
of which the compiler enforces:

- **Before a [`Select`](https://osysharp.com/reference/query/select/) projection**, only `OrderBy`/`OrderByDescending` are accepted — **`ThenBy` is
  not**. Sort the projected result instead, or project into a `class` and sort that.
- **Before a [`GroupBy`](https://osysharp.com/reference/query/group-by/)**, nothing but `Where` is accepted. Sorting the rows that are about to be
  collapsed into groups would not mean anything; sort the *groups* after the projection, which is supported.

## Examples       {#examples}
Sorting by something computed, and sorting the result of a grouping (which is where you usually want it — the top N
by an aggregate):

```osy title="sort by an expression; sort groups by their aggregate" test app=query-ordering
class RegionTotal { public string Region; public decimal Total; }

entity Sale {
  [Required, MaxLength(60)] string Region;
  decimal Amount;
}

List<RegionTotal> TopRegions() {
  return Sale.GroupBy(s => s.Region)
             .Select(g => new RegionTotal { Region = g.Key, Total = g.Sum(s => s.Amount) })
             .OrderByDescending(r => r.Total)   // sort the GROUPS, by their aggregate
             .Take(5)
             .ToList();
}
```

## See also       {#see-also}
- [Skip / Take (paging)](https://osysharp.com/reference/query/paging/) — `Skip`/`Take`, and why an unsorted page is a bug
- [First / Single / Last / ElementAt](https://osysharp.com/reference/query/single-row/) — `Last()` and the ordering it requires
- [GroupBy (and HAVING)](https://osysharp.com/reference/query/group-by/) — sorting groups by an aggregate
- [Querying data](https://osysharp.com/reference/query/index/) — the shape of a query chain


---

<!-- https://osysharp.com/reference/query/deferred/ -->

# Query<T>

> Holds a query instead of its rows. A clause you write against a `Query<T>` joins the query rather than filtering rows already fetched, so a chain split across a binding — or across a function boundary — is still ONE SQL statement. Without it, splitting the chain reads every matching row and finishes the work in memory.

<!-- id: query-deferred · area: query · stability: stable · html: https://osysharp.com/reference/query/deferred/ -->

## Summary        {#summary}
A `Query<T>` is a query **that has not run yet** — and it is what a LINQ chain already is, whether or not you spell
it. Writing the type down matters in one place: when the query has to cross a function boundary, where a parameter
needs a type.

```osy title="one statement, written in two pieces" test app=query-deferred
entity Film {
  [Required, MaxLength(200)] string Title;
  int Year;
  decimal Rating;

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

string BestOfYear(int year) {
  Query<Film> candidates = Film.Where(f => f.Year == year);       // the filter …
  return candidates.OrderByDescending(f => f.Rating).First().Title;  // … and the pick, in ONE query
}
```

## Signature      {#signature}
```osy syntax
var         q = Film.Where(…);          // inferred — holds the query, does not run it
Query<Film> q = Film.Where(…);          // the same thing, written down
Film Best(Query<Film> q) { … }          // a parameter — the caller supplies the query
q.OrderBy(…).Take(…)                    // a clause JOINS the query
foreach (var f in q) { … }              // read as rows: HERE the query runs
q.ToList()                              // …and this is how you say "run it now" on purpose
```

## Description    {#description}

### C# spells it `IQueryable<T>`, and that works too   {#iqueryable}
If you reach for C#'s name, write it — `IQueryable<Film>` **is** `Query<Film>`, in every position, and there is no
conversion or preference between them:

```osy title="the C# spelling, doing exactly the same thing" test app=query-deferred
Film PickBestOf(IQueryable<Film> candidates) { return candidates.OrderBy(f => f.Rating).First(); }
```

⚠ Its SIBLING interfaces are a different answer, and the reason is worth knowing: `IEnumerable<T>`,
`IReadOnlyList<T>`, `IList<T>` are refused, because Osy# spells those `T[]` and `List<T>` — **which are themselves
C#**, so the refusal leaves you writing C# and costs one round trip. `IQueryable<T>` has no such alternative
spelling, so refusing it would push you off C# onto an Osy#-only word. That is why one is accepted and the others
are taught.

### A plain `var` is the same thing   {#var}
You do not have to write the type. `var` infers it, exactly as `var q = db.Orders.Where(…)` infers `IQueryable` in
C#, so a chain split across a binding is still one statement:

```osy title="`var` and the written type are the same query" test app=query-deferred
// Both are ONE statement: WHERE, ORDER BY and LIMIT 1 together.
string BestInferred(int year) {
  var candidates = Film.Where(f => f.Year == year);
  return candidates.OrderByDescending(f => f.Rating).First().Title;
}

string BestSpelled(int year) {
  Query<Film> candidates = Film.Where(f => f.Year == year);
  return candidates.OrderByDescending(f => f.Rating).First().Title;
}
```

### What ends the deferral — `.ToList()`, and a terminal   {#ending}
The chain is deferred until something asks for the answer. Two things do, and they are how you say "read it here":

```osy title="`.ToList()` is `run it now`" test app=query-deferred
// `.ToList()` reads the rows HERE. The sort below runs over the list, in memory — which is what you want when
// you are going to ask the same rows several questions.
string BestThenCount(int year) {
  var rows = Film.Where(f => f.Year == year).ToList();
  var best = rows.OrderByDescending(f => f.Rating).First().Title;
  return best + " of " + rows.Count.ToString();
}
```

A **terminal** — `First`, `Single`, `Count`, `Any`, `Sum` and friends — ends it too, because it has produced the
answer. And a **declared row type** asks for the rows at the binding: `Film[] rows = Film.Where(…);`.

### Passing a query to a function   {#across-functions}
A `Query<T>` parameter is the half that has no other spelling: the caller decides **which** rows, the helper decides
what to **do** with them, and it is still one statement.

```osy title="the caller filters, the helper orders and pages" test app=query-deferred
string[] TopTitles(Query<Film> src, int take) {
  return src.OrderByDescending(f => f.Rating).Take(take).Select(f => f.Title);
}

string[] BestOf(int year) { return TopTitles(Film.Where(f => f.Year == year), 3); }
string[] BestEver()       { return TopTitles(Film.Where(f => f.Rating > 0m), 10); }
```

The helper is **composed into** each call rather than called, so its body must be a single `return <query>;` (or an
`=> <query>` expression body). A body that does more than that has no expression to compose, and says so.

### Reading the rows   {#reading-rows}
Used anywhere rows are wanted — a `foreach`, a `return`, an argument — a `Query<T>` **is** the rows, and that is
where it runs. It is only special at the head of a chain.

```osy title="the same binding, read as rows" test app=query-deferred
int CountOfYear(int year) {
  Query<Film> candidates = Film.Where(f => f.Year == year);
  return candidates.Count();                    // SELECT count(*) … WHERE Year = @year
}

string[] TitlesOfYear(int year) {
  Query<Film> candidates = Film.Where(f => f.Year == year);
  var titles = new List<string>();
  foreach (var f in candidates) { titles.Add(f.Title); }   // runs here
  return titles.ToArray();
}
```

### Using one twice runs it twice   {#twice}
⚠ **This is the one thing to know, and it applies to the inferred `var` as much as to the written type.** A deferred
query is a description, not a result — so each use goes to the database, exactly as enumerating a C# `IQueryable`
twice is two round trips. When you want the rows once and then several answers from them, say so with
[ToList](https://osysharp.com/reference/query/tolist/):

```osy title="two answers from one read" test app=query-deferred
string Report(int year) {
  var rows = Film.Where(f => f.Year == year).ToList();   // read ONCE …
  var howMany = rows.Count();                            // … then ask it twice, in memory
  var best = rows.Max(f => f.Rating);
  return howMany.ToString() + " films, best " + best.ToString();
}
```

### What it will not hold   {#refusals}
The initializer has to be a query that has not run. The two ways to get that wrong are different mistakes, and each
is named:

| You wrote | Why it is refused |
|---|---|
| `Query<Film> q = Film.Where(…).First();` | `First` **runs** it — what you have is the answer, not the query. Write the terminal where you use it. |
| `Query<Film> q = rows.Where(…);` over a `Film[]` | those rows have already been read. To hold rows, declare the list type: `Film[]`, `List<Film>`. |
| `Query<Film> t = Film.Select(f => f.Title);` | the query yields `string`. The declared element is checked against what the query **yields**, which a projection changes — write `Query<string>`. |

### Why it is not a value you can store   {#compile-time}
A `Query<T>` is resolved where it is used, at compile time — it is not an object that exists while the program runs,
so it cannot be put in a field, returned from a function, or held in a list. That is a **security** boundary before
it is an economy: a query becomes SQL, and a query that could travel as a value could travel to a function running
in the browser. Composing in the compiler means nothing new crosses the wire.

To hand rows to something that outlives the expression, materialise them with [ToList](https://osysharp.com/reference/query/tolist/).

### Do the write terminals compose too?   {#write-terminals}
Yes — a `Query<T>` may end in [`.Delete()`](https://osysharp.com/reference/query/delete/), [`.Update(…)`](https://osysharp.com/reference/query/update/) or
[`.Insert(…)`](https://osysharp.com/reference/query/insert-from/) exactly as it ends in `.Count()`: the helper's chain composes into the caller's
statement at compile time, so `int PurgeVia(Query<Order> doomed) { return doomed.Delete(); }` is still one
statement, with the caller's security floor.

## See also       {#see-also}
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — the clauses a `Query<T>` composes
- [ToList](https://osysharp.com/reference/query/tolist/) — reading the rows once, when you want several answers from them
- [Skip / Take (paging)](https://osysharp.com/reference/query/paging/) — `Skip` / `Take`, the clause most worth composing rather than filtering
- [LINQ over a local list](https://osysharp.com/reference/query/in-memory-linq/) — the other side: LINQ over rows you already hold


---

<!-- https://osysharp.com/reference/query/index/ -->

# Querying data

> How you read data in Osy#. You write C# LINQ; it becomes one SQL statement. The rules that follow from that are the whole model: the predicate runs in the database (not over rows you fetched), and a verb that cannot become SQL is refused rather than run quietly over everything.

<!-- id: query-index · area: query · stability: stable · html: https://osysharp.com/reference/query/index/ -->

## Summary        {#summary}
You query data by writing **C# LINQ over your entities**. There is no query language to learn, no repository to
write, and no mapping layer to configure:

```osy title="a query, and what it is" test app=query-index
entity Customer {
  [Required, MaxLength(80)] string Name;

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

entity Order {
  [Required, MaxLength(40)] string Code;
  decimal Total;
  bool Cancelled;
  Customer? Customer;

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }   // an entity with no security block is denied to everyone
}

List<Order> BigOpenOrders(decimal floor) {
  return Order.Where(o => o.Total > floor && !o.Cancelled)
              .OrderByDescending(o => o.Total)
              .Take(20)
              .ToList();
}
```

That is **one SQL statement**. Two things follow from it, and together they are the whole mental model:

1. **The predicate runs in the database.** It is not a filter over rows you already fetched. A table with ten million
   rows costs you the twenty you asked for.
2. **A verb that cannot become SQL is refused**, loudly, at compile time — never run quietly over the whole table.

## Description    {#description}

### 1. Three things you can query   {#sources}
The same verbs work over three different sources, and knowing which one you are on tells you what it costs:

| Source | What it is | Cost |
|---|---|---|
| **An entity** — `Order.Where(…)` | the table | SQL. You pay for the rows you asked for. |
| **A collection** — `order.Lines.Where(…)` | a parent's children ([Child collections (navigating a relation)](https://osysharp.com/reference/query/collections/)) | SQL, correlated to the parent. |
| **A local list** — `items.Where(…)` | a `List<T>` you built in code ([LINQ over a local list](https://osysharp.com/reference/query/in-memory-linq/)) | memory. You already have the elements. |

The spelling is identical on purpose — you do not learn two query languages. But an entity query is a *question to the
database*, and a list query is a *loop over what you are holding*. No security rule applies to values you constructed
yourself, and a list of ten million elements is ten million elements in memory.

### 2. What order do the verbs go in?   {#shape}
A query is built the way you would build it in C#: narrow, then order, then page, then finish.

```osy syntax
<Entity>
  .Where(o => <predicate>)          // ONE predicate — combine conditions with && inside it
  .OrderBy(o => k).ThenBy(o => k2)  // any number of keys
  .Skip(n).Take(m)                  // a page (the counts may be runtime values)
  .Include(o => o.Lines)            // pre-load related rows
  .ToList();                        // materialise
```

**A chain may carry more than one `Where`, and they compose** — `xs.Where(a).Where(b)` is `xs.Where(a && b)`,
exactly as in LINQ, and it is still one statement. The two lambdas need not name their parameter the same.

**And the chain need not all be in one place.** Bind it to a `var` and the clauses you add later still join the
SAME query — a chain is deferred until something asks for the answer, exactly as in C#. `.ToList()` is how you say
"read it here". See [Query<T>](https://osysharp.com/reference/query/deferred/), which is also the type you write when a query crosses a function boundary.

The chain ends in a **terminal**, and the terminal is what decides the shape of the answer:

| You want | Terminal | Page |
|---|---|---|
| the rows | `.ToList()` | [ToList](https://osysharp.com/reference/query/tolist/) |
| one row | `.First()` · `.Single()` · `.Last()` · `.FirstOrDefault()` … | [First / Single / Last / ElementAt](https://osysharp.com/reference/query/single-row/) |
| a number | `.Count()` · `.Sum(…)` · `.Average(…)` · `.Min/Max(…)` | [Sum / Average / Min / Max / Count](https://osysharp.com/reference/query/aggregates/) |
| a yes/no | `.Any(…)` | [Where / Single / Count](https://osysharp.com/reference/query/where/) |
| a reshaped row | `.Select(o => new T { … })` | [Select (projections)](https://osysharp.com/reference/query/select/) |
| a row per group | `.GroupBy(…).Select(g => …)` | [GroupBy (and HAVING)](https://osysharp.com/reference/query/group-by/) |

### 3. What is NOT there, and why   {#refusals}
The refusals are deliberate, and each is the same principle: **a verb either becomes SQL or it is refused.** The
alternative — quietly fetching the table and finishing the job in memory — is how an app works fine on your laptop and
falls over on real data.

- **`TakeWhile` / `SkipWhile`** are recognised and refused: they cannot be expressed in SQL. Use `Where` + `OrderBy` +
  `Skip`/`Take`. (C# on a database refuses them too.)
- **`All`, `Contains`, `Aggregate`** as query verbs do not exist. `All(p)` is `!Any(!p)`; for `Contains`, see
  [Dynamic IN (list.Contains in a query)](https://osysharp.com/reference/query/dynamic-in/).
- **Full-text and vector search** are not chain verbs — they are predicates *inside* a `Where`, and they need a
  `[Searchable]` property. See [[Searchable]](https://osysharp.com/reference/memory/searchable/).

### 4. A query sees what you have already written   {#read-your-writes}
A server function commits when it **returns** ([function](https://osysharp.com/reference/function/declaration/)) — so the rows you create partway through it
are not in the database yet. A query in the same function sees them anyway:

```osy title="create, then read back — no commit, no ceremony" test app=query-index
List<Order> BusiestFirst() {
  var a = new Order { Code = "A", Total = 20m };
  var b = new Order { Code = "B", Total = 90m };
  var c = new Order { Code = "C", Total = 40m };

  return Order.Where(o => o.Total > 10m)      // …matches the three above AND anything already stored
              .OrderByDescending(o => o.Total)
              .ToList();                       // …and they are ordered together: 90, 40, 20
}
```

Your pending rows are matched by the `Where`, **sorted into** the order you asked for, paged by `Skip`/`Take`, and
counted by `Count()`. You do not have to commit first, and you should not: committing early would give up the
all-or-nothing guarantee that a fault discards everything the function wrote.

```osy title="proof: the three uncommitted rows come back in order" run app=query-index
[Test]
void A_query_orders_the_rows_this_function_has_not_committed_yet() {
  var busiest = BusiestFirst();

  Assert.Equal(3, busiest.Count);
  Assert.Equal("B", busiest[0].Code);   // 90 — sorted WITH the pending rows, not appended after them
  Assert.Equal("C", busiest[1].Code);   // 40
  Assert.Equal("A", busiest[2].Code);   // 20
}
```

**A pending *edit* counts too**, not just a pending create. Change a field and the very next `Where` decides on the
value you just wrote: a row your edit now matches is returned, and one it no longer matches is not — so *"mark these
delivered, then ask which are still outstanding"* answers the question you actually asked.

```osy title="the filter decides on the value you just wrote" run app=query-index
[TestFixture]
void Stored() {
  var o = new Order { Code = "A", Total = 20m };
  UnitOfWork.Commit();                                     // A is now a stored row, Total = 20
}

[Test(Stored)]
void A_pending_edit_decides_the_filter() {
  var order = Order.Single(o => o.Code == "A");
  order.Total = 500m;                           // a pending edit to a STORED row — no UnitOfWork.Commit()

  Assert.Equal(1, Order.Where(o => o.Total > 100m).ToList().Count);   // it joined the set…
  Assert.Empty(Order.Where(o => o.Total < 100m).ToList());            // …and left the one it was in
  Assert.Equal(500m, order.Total);
}
```

`Count()` and `Any()` answer from the same reconciled set, so `Where(p).Count()` and `Where(p).ToList().Count` cannot
disagree.

**A filter on a *reference* works the same way**, on rows and on links that are both still pending. This is the shape
worth seeing, because a parent and its children are usually created together — neither exists in the database yet, and
the question is still answered from what this function has written.

```osy title="a reference filter over rows that are not committed yet" run app=query-index
[Test]
void A_pending_row_is_found_by_the_reference_you_just_set() {
  var mine   = new Customer { Name = "Mine" };
  var theirs = new Customer { Name = "Theirs" };

  var a = new Order { Code = "A", Total = 10m, Customer = mine };
  var b = new Order { Code = "B", Total = 20m, Customer = theirs };

  Assert.Equal(1, Order.Where(o => o.Customer == mine).ToList().Count);    // …and not B

  a.Customer = theirs;                                                     // re-point it, still no UnitOfWork.Commit()
  Assert.Empty(Order.Where(o => o.Customer == mine).ToList());             // it left the set it was in…
  Assert.Equal(2, Order.Where(o => o.Customer == theirs).ToList().Count);  // …and joined the other
}
```

**One honest limit: `Sum` / `Average` / `Min` / `Max` see only committed rows.** A value aggregate does not include
rows you created and have not committed — so a total is quietly *short* by exactly the rows you just added. `Count()`,
`Any()` and ordinary queries all include them; only the value aggregates do not. If you need one over rows you have
just created or changed, `UnitOfWork.Commit()` first.

### 5. Where the divergences from C# are   {#divergences}
Faithful C# is the goal, so the handful of places the language deliberately differs are worth knowing, because each
one is a bug waiting to happen if you assume otherwise:

- **`Sum` / `Average` / `Min` / `Max` return null on an empty set**, not zero — because SQL does. See
  [Sum / Average / Min / Max / Count](https://osysharp.com/reference/query/aggregates/), which is the page that will save you the most debugging.
- **`Average` is always `decimal`**, whatever the selector's type.
- **`ThenBy` is a synonym for a chained `OrderBy`.** The keys accumulate into one composite sort — a second `OrderBy`
  does *not* re-sort as it would in C#.
- **`Last` requires an `OrderBy`.** "The last row" is meaningless without an order, and the database will not guess.

## See also       {#see-also}
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — the predicate, and `Single` vs `FirstOrDefault` vs `Count`
- [OrderBy / ThenBy](https://osysharp.com/reference/query/ordering/) — `OrderBy` / `ThenBy`, and where they may appear
- [Sum / Average / Min / Max / Count](https://osysharp.com/reference/query/aggregates/) — `Sum` / `Average` / `Min` / `Max` / `Count`, and null-on-empty
- [GroupBy (and HAVING)](https://osysharp.com/reference/query/group-by/) — `GroupBy`, per-group aggregates, and HAVING
- [Select (projections)](https://osysharp.com/reference/query/select/) — projections, and what may precede and follow one
- [Include (pre-loading relations)](https://osysharp.com/reference/query/include/) — pre-loading related rows
- [Child collections (navigating a relation)](https://osysharp.com/reference/query/collections/) — a parent's children, and the FK query you should not write
- [First / Single / Last / ElementAt](https://osysharp.com/reference/query/single-row/) — `First` / `Single` / `Last` / `ElementAt`, and what each does when there is no row
- [Join / LeftJoin / SelectMany](https://osysharp.com/reference/query/joins/) — `Join` / `LeftJoin` / `SelectMany`
- [Traverse (walking a graph)](https://osysharp.com/reference/query/traverse/) — walking a graph to any depth
- [Skip / Take (paging)](https://osysharp.com/reference/query/paging/) — `Skip` / `Take`
- [LINQ over a local list](https://osysharp.com/reference/query/in-memory-linq/) — the same verbs over a `List<T>` you built yourself
- [The security model](https://osysharp.com/reference/security/index/) — why a grant is part of the query rather than a check you remember


---

<!-- https://osysharp.com/reference/query/select/ -->

# Select (projections)

> Reshape what a query returns: one column, an anonymous row, or a `class` you declared. The projection becomes the SQL SELECT list, so the columns you did not ask for are never read — which is the point of it. `Select` must be the LAST clause of the chain; only `Where`, `OrderBy` and `Take` may precede it, and only `Distinct()` may follow.

<!-- id: query-select · area: query · stability: stable · html: https://osysharp.com/reference/query/select/ -->

## Summary        {#summary}
`Select` says **which columns you want**, and in what shape:

```osy title="three shapes of projection" test app=query-select
class OrderRow {
  public string Code;
  public decimal Total;
}

entity Order {
  [Required, MaxLength(40)] string Code;
  [MaxLength(60)] string Region;
  decimal Total;
  bool Cancelled;
}

List<string> Codes() {
  return Order.Select(o => o.Code).ToList();               // one column → a List<string>
}

List<OrderRow> Rows() {
  return Order.Where(o => !o.Cancelled)
              .Select(o => new OrderRow { Code = o.Code, Total = o.Total })
              .ToList();                                                       // → a class you declared
}
```

The projection becomes the SQL `SELECT` list, so the columns you did not name are **never read** — no wide rows over
the wire, and no entity to materialise.

## Signature      {#signature}
```osy syntax
<Query>.Select(o => o.Column)              // a scalar     → List<string> / List<decimal> / …
<Query>.Select(o => o.Total * 2)           // an expression scalar
<Query>.Select(o => new T { A = o.X, … })  // a `class` you declared → List<T>
<Query>.Select(o => new { o.X, o.Y })      // anonymous — usable on the spot, cannot be returned
```

## Description    {#description}

### It must be the last clause   {#last-clause}
A `Select` **ends** the chain. The compiler enforces a narrow window around it, and the restrictions are not
arbitrary — each one is a thing SQL cannot do once the rows have been reshaped:

**Before a `Select`, only:** `Where` · `OrderBy` / `OrderByDescending` · `Take`.

- **`ThenBy` may not precede a `Select`.** Sort the projected result instead.
- **`Skip` may not precede a `Select`** (though `Take` may). To page a projection: project into a `class` and page
  that, or page the entity query and project after.
- **`Include` may not precede a `Select`** — and it would be meaningless if it could: [`Include`](https://osysharp.com/reference/query/include/)
  pre-loads *related entity rows*, and a projection does not return entity rows at all. Just select what you want.

**After a `Select`, only:** `ToList()` (a no-op — the projection already materialises) and `Distinct()`.

There are no terminals over a projection: no `First()`, no `Count()`, no `Sum()`. Do the aggregate over the entity
query instead ([Sum / Average / Min / Max / Count](https://osysharp.com/reference/query/aggregates/)), or group it ([GroupBy (and HAVING)](https://osysharp.com/reference/query/group-by/)).

### The one exception: `Distinct().Count()`   {#distinct-count}
The one composition allowed after a projection, because it is a single SQL expression — and it answers a question you
genuinely cannot get another way:

```osy title="how many DIFFERENT regions have we sold into" test app=query-select
int RegionsSoldInto() {
  return Order.Select(o => o.Region).Distinct().Count();   // → COUNT(DISTINCT region)
}
```

`Distinct()` over a scalar projection then `Count()` becomes `COUNT(DISTINCT col)`. It must be the last clause, and
the projection must be a scalar. See [Distinct](https://osysharp.com/reference/query/distinct/).

### Which shape to reach for   {#shapes}
- **A scalar** (`o => o.Code`) when you want a list of values — ids to pass on, codes to render, amounts to sum in
  code. You get a real `List<T>`.
- **A `class`** when you want rows with names, especially across a function boundary. This is the workhorse: declare
  the shape, project into it, return it. It is a plain data shape ([class methods](https://osysharp.com/reference/class/methods/)) — no entity, no tracking, no
  lazy loading, nothing to surprise you later. A class projection can also back a reactive `live var` in a component —
  a live list of the shape you render, refreshing on commit ([The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/)).
- **Anonymous** (`o => new { o.Code, o.Total }`) only where the result is consumed on the spot. It has no name, so it
  cannot be a return type.

### It does not change what security allows   {#security}
A projection narrows the **columns**, never the **rows**. The [read rules](https://osysharp.com/reference/security/entity-security/) are compiled into
the same statement, so `Select` cannot be used to see a row you were not granted — and a
[field mask](https://osysharp.com/reference/security/entity-security/) (`deny read PasswordHash when …`) still applies to the column you projected.
Projecting is a performance and shape decision, not an access one.

## Examples       {#examples}
Projecting a computed value, and a narrow row for a list view — the common case, and the one that keeps a grid fast:

```osy title="a list view fetches four columns, not the whole row" test app=query-select
class OrderCard {
  public string Code;
  public decimal Total;
  public decimal WithVat;
  public bool Big;
}

List<OrderCard> Cards(decimal bigFrom) {
  return Order.Where(o => !o.Cancelled)
              .OrderByDescending(o => o.Total)
              .Take(50)
              .Select(o => new OrderCard {
                Code    = o.Code,
                Total   = o.Total,
                WithVat = o.Total * 1.2m,        // computed in the database
                Big     = o.Total > bigFrom })   // a captured local works, like any parameter
              .ToList();
}
```

### Where did my row come in the order?   {#indexed}
`Select((s, i) => …)` — C#'s index-aware projection — works over stored rows once the chain names an order: `i` is
the row's 0-based position in that order, computed by the database. The partitioned forms (per-group ranks, the
previous row's value) are the [window functions](https://osysharp.com/reference/query/window/).

## See also       {#see-also}
- [GroupBy (and HAVING)](https://osysharp.com/reference/query/group-by/) — a projection with a `GroupBy` in front of it: one row per group
- [Distinct](https://osysharp.com/reference/query/distinct/) — `Distinct()`, and `Distinct().Count()`
- [Include (pre-loading relations)](https://osysharp.com/reference/query/include/) — the opposite need: keep the entity rows, but pre-load their relations
- [class methods](https://osysharp.com/reference/class/methods/) — the `class` a projection targets
- [Querying data](https://osysharp.com/reference/query/index/) — where a projection sits in the chain


---

<!-- https://osysharp.com/reference/query/paging/ -->

# Skip / Take (paging)

> Page a query with Skip(n) (OFFSET) and Take(m) (LIMIT). The count can be a compile-time constant OR a runtime integer — a variable, parameter, or expression — so a page size chosen at runtime works directly.

<!-- id: query-paging · area: query · stability: stable · html: https://osysharp.com/reference/query/paging/ -->

## Summary        {#summary}
`Take(m)` limits a query to the first `m` rows; `Skip(n)` skips the first `n`. Together they page a result set
(`Skip(n).Take(m)`). The count for either can be a **compile-time constant** (`Take(20)`) **or a runtime
integer** — a variable, a parameter, or any int expression (`Take(pageSize)`) — exactly like the parameterized
`LIMIT`/`OFFSET` a database query uses. Order the query first (`OrderBy`) so paging is deterministic.

## Signature      {#signature}
```osy syntax
set.Where(p).OrderBy(k).Skip(<int>).Take(<int>).ToList()
//                       └ OFFSET      └ LIMIT   (each count: a constant OR a runtime int)
```

## Description    {#description}
- **`Take(m)`** returns at most the first `m` rows (SQL `LIMIT`). **`Skip(n)`** discards the first `n` rows
  (SQL `OFFSET`). `Skip(n).Take(m)` is the page-(n/m) window.
- **The count may be runtime.** `Take(pageSize)` / `Skip(page * pageSize)` accept an int **variable, parameter,
  or expression** — not just a literal. The value is read when the query runs (like a bound SQL parameter), so a
  page size the caller chose at runtime just works.
- **The count must be an integer.** A non-integer argument is a compile error.
- **Take never throws and never over-returns.** Asking for more rows than exist returns all of them; `Take(0)`
  returns none — matching C#.
- **Order first for determinism.** Without an `OrderBy`, the database may return any rows; page a *sorted* query.
- **What it composes with.** `Where`, [`OrderBy`/`ThenBy`](https://osysharp.com/reference/query/ordering/), [`Include`](https://osysharp.com/reference/query/include/), `Distinct`,
  and the single-row terminals. **`Skip` may not precede a [`Select`](https://osysharp.com/reference/query/select/) projection** — `Take` may, and
  `Skip` may not (page first and project after, or project into a `class` and page the result). Neither may precede a
  [`GroupBy`](https://osysharp.com/reference/query/group-by/): only `Where` may.

## Examples       {#examples}
```osy title="paging with a runtime page size" test app=query-paging
entity Order {
  [Required] string Name;
  decimal Total;
}

// The page and size are PARAMETERS — chosen by the caller at runtime.
Order[] PageOrders(int page, int size) {
  return Order
    .OrderByDescending(o => o.Total)
    .Skip(page * size)
    .Take(size)
    .ToList();
}

// A constant count still works exactly as before.
Order[] TopFive() {
  return Order.OrderByDescending(o => o.Total).Take(5).ToList();
}

// Take clamps: n larger than the row count returns all rows; 0 returns none.
int HowMany(int n) {
  var rows = Order.OrderBy(o => o.Name).Take(n).ToList();
  return rows.Count;
}
```

## See also       {#see-also}
- [Union / Concat / Intersect / Except](https://osysharp.com/reference/query/set-operators/) — Union / Concat / Intersect / Except over paged query sets
- [List OrderBy (in-memory)](https://osysharp.com/reference/function/list-orderby/) — OrderBy / Take on an in-memory `List<T>` (not a database set)


---

<!-- https://osysharp.com/reference/query/sorting-in-memory/ -->

# Sorting rows the client holds

> A sequence the client already holds — a component's `T[]` rows parameter, a `List<T>` — sorts with `OrderBy` / `OrderByDescending`, and the key is named by a SELECTOR, never by a string. That is what lets the USER pick the sort: a column already carries its selector, so naming the column names the sort.

<!-- id: query-sorting-in-memory · area: query · stability: preview · html: https://osysharp.com/reference/query/sorting-in-memory/ -->

## Summary        {#summary}
Rows the client already has sort in memory, and the key is a **selector**:

```osy title="the two spellings, both C#" syntax
rows.OrderBy(x => x.Title)     // a key lambda
rows.OrderBy(column.Value)     // the selector passed directly — C#'s method-group form
```

There is no string form. `OrderBy("Title")` does not exist here for the same reason a column's value is not named by
a string: a rename compiles and fails at runtime, and the compiler cannot check what it cannot see.

## Signature      {#signature}
```osy syntax
rows.OrderBy(<selector>)             // ascending
rows.OrderByDescending(<selector>)   // descending

// <selector> is either:
x => x.Field                         // a lambda taking ONE parameter — the row
column.Value                         // a Func<row, key> value
```

The receiver is a sequence the client **holds**: a component's `T[]` parameter, or a `List<T>`. A `.OrderBy` on a
query member is a different thing — see [OrderBy / ThenBy](https://osysharp.com/reference/query/ordering/) — and folds into the server read.

## Description    {#description}

### The selector IS the sort key   {#selector}
Because a selector is a value, the sort key can be chosen at runtime — which is the whole of click-to-sort:

```osy title="a sortable grid, in full" test app=query-sorting-in-memory
entity Report { [MaxLength(80)] string Title; decimal Total; }

class Column<T> { public string Label; public Func<T, string> Value; }

[Composable] component Grid<T>(T[] rows, Column<T>[] columns) {
  Column<T> sortBy = columns[0];
  action SortBy(Column<T> c) { sortBy = c; }
  render {
    Stack {
      Row {
        foreach (var h in columns) {
          Pressable(onClick: () => SortBy(h)) { Text(h.Label); }
        }
      }
      foreach (var r in rows.OrderBy(sortBy.Value)) {
        Row { foreach (var c in columns) { Text(c.Value(r)); } }
      }
    }
  }
}
```

The grid names no entity and no field. It works for every row type, because a [generic class](https://osysharp.com/reference/class/generics/)
carries the selector and the selector carries the key.

### Where it runs, and why you do not choose   {#execution-side}
A sequence **passed in** has already been fetched, so sorting it is in-memory work over rows on screen. A query
member still has a query behind it, so `.OrderBy` on one folds into the **server** read — which is the right answer
when the query is [paged](https://osysharp.com/reference/query/paging/), because sorting the client's window would order the wrong rows.

You never spell that difference. It follows from where the rows came from.

### A runtime key over a server query   {#runtime-key}
Not supported: a server query's `ORDER BY` is compiled into SQL, so its key must be written out. Let the user pick
the column by sorting the rows the client holds, as above. The compiler says so if you try.

## Examples       {#examples}

Descending, and a computed key — anything the selector can express:

```osy title="a computed sort key" syntax
rows.OrderByDescending(r => r.Total)
rows.OrderBy(r => r.Total > 1000 ? "large" : "small")
```

## Errors         {#errors}

| What you wrote | What you get |
|---|---|
| `rows.OrderBy(column.Run)` where `Run` is an `Action` | *needs a selector that RETURNS the key to sort by, and this one returns nothing.* |
| a `Column<User>` selector over `Report` rows | *this selector reads a User, but the rows are Report.* |
| `rows.OrderBy(x => x.Owner)` (an entity) | *a sort key must be a comparable value.* |
| `Report.OrderBy(col.Value)` (a server query) | *a server query's sort key is compiled into SQL … sort the rows the client already holds.* |
| `var copied = liveRows.ToList();` | *'copied' reads the live member 'liveRows', but 'copied' is not itself `live`* — see below. |

⚠ **Deriving from a query needs `live`.** A non-live field is evaluated once, when the component is created, before
the query has loaded — so it keeps an empty value forever, and the page renders with no rows and no error. Write
`live var copied = …`.

## See also       {#see-also}
- [OrderBy / ThenBy](https://osysharp.com/reference/query/ordering/) — `OrderBy`/`ThenBy` on a SERVER query, compiled into SQL
- [Func<T, R>](https://osysharp.com/reference/ui/function-value/) — `Func<T, R>` as a value: what a selector IS
- [Generic classes](https://osysharp.com/reference/class/generics/) — one `Column<T>` for every row type
- [Skip / Take (paging)](https://osysharp.com/reference/query/paging/) — `Skip`/`Take`, and why a paged query sorts server-side


---

<!-- https://osysharp.com/reference/query/aggregates/ -->

# Sum / Average / Min / Max / Count

> Fold rows down to a single number — a query or a `List<T>` you already hold. The one thing to know before you use them: **Min/Max/Average answer null over no rows** — they have no zero identity, so those you must answer for (`Max(…) ?? 0`). **Sum and Count have one**: Sum over no rows is 0, exactly as `Enumerable.Sum()` is in C#, so you take it straight as a decimal and no `?? 0m` is needed, and Count is honestly 0 with Any false. Average is always decimal. A query folds in the database and a list folds in memory, with the same answers either way.

<!-- id: query-aggregates · area: query · stability: stable · html: https://osysharp.com/reference/query/aggregates/ -->

## Summary        {#summary}
An aggregate folds a query into one value, **in the database** — the rows are never fetched:

```osy title="one number, computed by the database" test app=query-aggregates
entity Order {
  [Required, MaxLength(40)] string Code;
  decimal Total;
  bool Cancelled;
  DateTime PlacedAt;
}

decimal Revenue() {
  return Order.Where(o => !o.Cancelled).Sum(o => o.Total);   // ← a plain decimal. No rows is 0, as in C#.
}

int OpenCount() {
  return Order.Count(o => !o.Cancelled);
}
```

## Signature      {#signature}
```osy syntax
<Query>.Count()                    // int  — how many rows
<Query>.Count(o => predicate)      // int  — how many match
<Query>.Any()  /  .Any(o => p)     // bool — is there at least one

<Query>.Sum(o => value)            // the selector's type — 0 over no rows, so take it as-is
<Query>.Average(o => value)        // decimal, NULLABLE   (always decimal)
<Query>.Min(o => value)            // the selector's type, NULLABLE
<Query>.Max(o => value)            // the selector's type, NULLABLE
```

`Sum` and `Average` need a **numeric** selector. `Min`/`Max` work on any scalar (dates and strings included). All four
require the selector — there is no argument-less `Sum()`.

## Description    {#description}

### What does an aggregate answer when there are no rows?   {#null-on-empty}
It depends on the aggregate, and the split is the same one C# makes:

| aggregate | over no rows | why |
|---|---|---|
| `Sum` | **0** | `Enumerable.Sum()` over an empty sequence is 0. "We spent nothing under Outreach" *is* zero, and a report printing nothing there is wrong in the one direction a reader cannot see. |
| `Count` | **0** | "how many" is honestly none. |
| `Any` | **false** | |
| `Min` · `Max` · `Average` | **null** | they have no zero identity. An empty catalogue has no cheapest price, and "from £0" is a lie — C# throws rather than invent one, and here you get null so you can say what to show. |

So `Sum` needs no `??` and never did — take it straight:

```osy title="Sum takes no ceremony; Min does" test app=query-aggregates
decimal SpendFor(Order o) {
  return Order.Where(x => x.Code == o.Code).Sum(x => x.Total);   // plain decimal — no rows is 0m
}

decimal? CheapestOpen() {
  return Order.Where(o => !o.Cancelled).Min(o => o.Total);       // stays NULLABLE — no orders has no cheapest
}
```

⚠ **This is one rule with three implementations, and they agree deliberately** — the SQL the database runs, the
server's own evaluator, and the client's. SQL's bare `SUM` over no rows *is* NULL, so the platform restores the zero
identity rather than letting the same expression answer differently depending on where it ran.

⚑ If the difference between *"totals zero"* and *"there is nothing here"* genuinely matters to you — a refund path,
say — ask that question directly with `Any()` or `Count()`, which is what it actually is. Do not try to read it out
of a `Sum`.

### I already tested that it is not empty — do I still need the `??`?   {#guarded}
**No.** A `Min`/`Max`/`Average` is null for exactly one reason — no rows — so a branch that runs only when the
source HAS rows is not nullable, and the compiler reads it that way. Both spellings of the local below are accepted,
and the value goes straight into a non-nullable field:

```osy title="the guard is a guard" test app=query-aggregates-guarded
entity Job { [Required, MaxLength(60)] string Title; int SortOrder; }

void AddToTheEnd(string title) {
  var next = Job.Any() ? Job.Max(j => j.SortOrder) + 1 : 1;      // `int`, not `int?`
  new Job { Title = title, SortOrder = next };
}

void AddToTheEndTheOtherWay(string title) {
  int next = Job.Count() == 0 ? 1 : Job.Max(j => j.SortOrder) + 1;   // the same, guarded the other way round
  new Job { Title = title, SortOrder = next };
}
```

`Any()`, `Any(p)`, `Count()`, `Count` and `Length` all read as the emptiness test, in either polarity and under a
`!`. What matters is that the guard tests **the same source the aggregate reads**.

⚠ **A `Where(…)` in between breaks it, and that is not a limitation — it is the truth.** `jobs.Any() ?
jobs.Where(j => j.Done).Max(j => j.Order) : 0` is still refused, because a filter can empty a collection that had
rows. Test what the aggregate actually reads, or answer for absence with `?? 0`.

⚠ **This is the only narrowing the language does.** It is one syntactic shape decided inside one conditional
expression — not general `if (x != null)` flow analysis, which Osy# does not have. A null test in a preceding
statement does not narrow anything.

### `Average` is always decimal   {#average}
Whatever you select, `Average` gives you a `decimal` (nullable). Averaging `int` quantities gives `2.5m`, not `2` —
which is what you meant, and what SQL does. Use [`Convert`](https://osysharp.com/reference/function/convert/) if you need it back as another type.

### An aggregate runs in the database, not in your loop   {#in-the-database}
An aggregate is one round trip that returns one value. Do not fetch rows to add them up yourself:

```osy title="the difference is the whole table" test app=query-aggregates
decimal Wrong() {
  decimal sum = 0m;
  foreach (var o in Order.ToList()) { sum = sum + o.Total; }   // ← fetches EVERY order to add them up
  return sum;
}

decimal Right() {
  return Order.Sum(o => o.Total);                              // ← the database adds them up; one number comes back
}
```

Both give the same answer on ten rows. On ten million, one of them is a `SELECT SUM(total)` and the other is an
outage.

### Can I aggregate a parent's children?   {#collections}
The same verbs work on a parent's children, correlated to that parent ([Child collections (navigating a relation)](https://osysharp.com/reference/query/collections/)):

```osy title="aggregate a parent's children" test app=query-aggregates
entity Invoice {
  [Required, MaxLength(40)] string Number;
  [ForeignKey(Invoice)] InvoiceLine[] Lines;
}

entity InvoiceLine {
  [Required] Invoice Invoice;
  [MaxLength(60)] string Sku;
  int Qty;
  decimal Amount;
}

decimal InvoiceTotal(Invoice inv) {
  return inv.Lines.Sum(l => l.Amount);            // one SQL statement, correlated to this invoice
}

bool HasBackorder(Invoice inv) {
  return inv.Lines.Any(l => l.Qty == 0);
}
```

### Does `Sum` work on a plain list, not just a query?   {#in-memory}
Yes — the same verbs work over a `List<T>` you already hold, including a list of [class](https://osysharp.com/reference/class/index/) values that
never came from the database. There is **no second dialect and no different answer**: an in-memory `Sum` is a plain
`decimal` and an empty list is `0m`, exactly as the query form is.

```osy title="the same Sum over a list you built yourself" test app=query-aggregates
class Weighing { public string Sku; public decimal Kg; }
```

```osy title="a list of class values totals the same way a query does" run app=query-aggregates
[Test]
void Summing_A_Plain_List() {
  var load = new List<Weighing>();
  load.Add(new Weighing { Sku = "A", Kg = 2m });
  load.Add(new Weighing { Sku = "B", Kg = 3m });

  decimal total = load.Sum(w => w.Kg);          // a plain decimal — no `??`, no nullable
  Assert.Equal(5m, total);

  decimal none = new List<Weighing>().Sum(w => w.Kg);
  Assert.Equal(0m, none);                       // an empty list is 0m, same as an empty query
}
```

⛔ So do **not** hand-roll an accumulator loop because you expect nullable trouble. `foreach (var w in load) { t = t
+ w.Kg; }` is longer, and it is not buying you anything the `Sum` was not already giving you.

## Examples       {#examples}
The full set, and the two ways to treat an empty result:

```osy title="every aggregate, and what empty means for each" test app=query-aggregates
class PriceBand { public decimal? From; public decimal? To; }

decimal AverageOrderValue() {
  return Order.Average(o => o.Total) ?? 0m;        // no orders → an average of nothing → call it 0
}

PriceBand Band() {
  return new PriceBand {
    From = Order.Min(o => o.Total),                // KEEP the null: an empty catalogue has no lowest price,
    To   = Order.Max(o => o.Total),                // and "from 0" would be a lie
  };
}

DateTime? LastOrderAt() {
  return Order.Max(o => o.PlacedAt);               // Min/Max work on dates and strings too, not just numbers
}
```

## See also       {#see-also}
- [GroupBy (and HAVING)](https://osysharp.com/reference/query/group-by/) — the same aggregates, once per group, with `HAVING`
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — `Count` / `Any` and the predicate they take
- [Child collections (navigating a relation)](https://osysharp.com/reference/query/collections/) — aggregating a parent's children
- [Querying data](https://osysharp.com/reference/query/index/) — why an aggregate is one statement and not a loop


---

<!-- https://osysharp.com/reference/query/tolist/ -->

# ToList

> Runs the query and materialises the rows as a `List<Entity>`. Until you call it, a query is a description of what you want; ToList is the moment it becomes rows you can walk, count and index. `.ToArray()` does the same and answers the fixed-size `Entity[]` instead.

<!-- id: query-tolist · area: query · stability: stable · html: https://osysharp.com/reference/query/tolist/ -->

## Summary        {#summary}
`.ToList()` **runs** the query. Up to that point you have built a description — a set of conditions, an order, a page
— and nothing has touched the database. `ToList` is where it executes and you get rows back, typed as `List<Entity>`
exactly as in C#.

## Signature      {#signature}
```osy syntax
<Entity>.Where(…).OrderBy(…).ToList()    →   List<Entity>    // materialised, and mutable
<Entity>.Where(…).OrderBy(…).ToArray()   →   <Entity>[]      // materialised, and fixed-size
<Entity>.Where(…).OrderBy(…)             →   <Entity>[]      // a chain that simply ends
```

## Description    {#description}

### When does the query actually run?   {#building}
The chain composes without executing. Only `ToList` (and the scalar calls — `Count`, `Any`, `Single`) go to the
database:

```osy title="compose, then run once" test app=query-tolist
entity Order {
  [Required] string Code;
  decimal Total;
}

List<Order> TopOrders(decimal min, int take) {
  return Order
    .Where(o => o.Total >= min)      // nothing has run yet
    .OrderByDescending(o => o.Total) // still nothing
    .Take(take)
    .ToList();                       // NOW it runs — one query, one round trip
}
```

### What you get back   {#the-result}
A `List<Entity>` — materialised rows. It has a `.Count`, it can be indexed, and it can be walked:

```osy title="using the result" test app=query-tolist
decimal SumOfTop(decimal min, int take) {
  var top = TopOrders(min, take);

  var total = 0m;
  foreach (var o in top) { total += o.Total; }   // walk it

  var first = top.Count > 0 ? top[0].Total : 0m;  // index it
  return total + first * 0m;
}
```

### Why does calling it twice do the work twice?   {#run-once}
Because `ToList` is the moment work happens, calling it twice does the work twice. Materialise into a local and use
that:

```osy title="materialise once, use many times" test app=query-tolist
string Describe(decimal min) {
  var orders = Order.Where(o => o.Total >= min).ToList();   // one query
  var count = orders.Count;
  var total = 0m;
  foreach (var o in orders) { total += o.Total; }
  return Convert.ToString(count) + " orders, " + Convert.ToString(total);
}
```

Writing `Order.Where(…).ToList()` twice in that function would run two identical queries — and, if a row changed in
between, give you two different answers to the same question.

### `ToList` or `ToArray` — which materialiser   {#tolist-or-toarray}
Both run the query and bring back the same rows. They differ only in the type you are left holding, and that is the
same difference C# draws:

- **`.ToList()` answers a `List<Entity>`** — you can `.Add` to it, so it is what you want when the rows are the start
  of something you are still assembling.
- **`.ToArray()` answers an `Entity[]`** — fixed-size, and the plainer statement when the rows are the answer.

A `List<T>` is accepted anywhere a `T[]` is asked for, because giving up `.Add` is always safe. The reverse is not: a
`T[]` is not a `List<T>`, and a function that wants one says so by materialising with `.ToList()`.

```osy title="the two materialisers, and the one-way conversion" test app=query-tolist
List<Order> Growing(decimal min) { return Order.Where(o => o.Total >= min).ToList(); }
Order[]     Fixed(decimal min)   { return Order.Where(o => o.Total >= min).ToArray(); }

// A List goes where an array is wanted — no conversion written, none needed.
Order[] Widened(decimal min) { return Order.Where(o => o.Total >= min).ToList(); }

// …and `.ToArray()` says it out loud, on a list you built yourself.
string[] Codes(decimal min) {
  var codes = new List<string>();
  foreach (var o in Order.Where(o => o.Total >= min).ToList()) { codes.Add(o.Code); }
  return codes.ToArray();
}
```

⚠ **`.ToArray()` takes a COPY.** Adding to the list afterwards does not change the array you already took — that is
what makes "fixed-size" mean anything.

### When you only want a number   {#count-instead}
If all you need is how many, do not materialise the rows to count them. `Count()` asks the database for the number and
brings back one integer instead of ten thousand rows. See [Where / Single / Count](https://osysharp.com/reference/query/where/).

## See also       {#see-also}
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — building the query `ToList` runs
- [Skip / Take (paging)](https://osysharp.com/reference/query/paging/) — `Skip` / `Take`, to materialise one page instead of everything
- [foreach](https://osysharp.com/reference/function/foreach/) — walking what came back


---

<!-- https://osysharp.com/reference/query/traverse/ -->

# Traverse (walking a graph)

> Walk a relation recursively — an org chart up to its root, a category tree down to its leaves, a bill of materials, a reply thread — and get back every row on the way. It is one recursive SQL query with cycle detection and a depth bound, which is the thing you cannot write with a loop of ordinary queries without paying a round trip per level.

<!-- id: query-traverse · area: query · stability: stable · html: https://osysharp.com/reference/query/traverse/ -->

## Summary        {#summary}
`Traverse` follows a relation over and over, and returns everything it reaches:

```osy title="everyone under a manager, however deep" test app=query-traverse
entity Employee {
  [Required, MaxLength(120)] string Name;
  Employee Manager;                                     // the relation to walk
  [ForeignKey(Manager)] Employee[] Reports;
  bool Active;
}

Employee[] EveryoneUnder(Employee boss) {
  return Employee.Traverse(
    from:   boss,
    follow: e => e.Reports,          // walk DOWN the collection
    depth:  10,
    where:  e => e.Active);          // …skipping anyone inactive (and everyone below them)
}
```

That is **one** query — a recursive one — not a loop that fires a query per level. It detects cycles and it is bounded
by `depth`, so a self-referencing row cannot hang it.

## Signature      {#signature}
```osy syntax
<Entity>.Traverse(
  from:          <seed> | [<seed>, <seed>],   // REQUIRED — one row, or several
  follow:        e => e.<Relation>,           // REQUIRED — an entity reference (up) or a collection (down)
  depth:         <positive int literal>,      // optional — default 10
  where:         e => <predicate>,            // optional — pruned at each hop
  match:         e => e.<Column>,             // optional — walk an edge table by VALUE (see below)
  bidirectional: true                         // optional — requires `match:`
)
```

**Named arguments only** — a positional argument is a compile error, because six positional arguments would be
unreadable and easy to get subtly wrong. `depth` must be an integer *literal*, not a variable.

## Description    {#description}

### Up or down — the direction is the relation you follow   {#direction}
`follow:` names a relation on the entity, and its *kind* decides which way you walk:

- **An entity reference** (`e => e.Manager`) walks **up** — from a row to the one it points at. Seed with an employee
  and you get their management chain to the root.
- **A collection** (`e => e.Reports`) walks **down** — from a row to the rows that point at it. Seed with a manager and
  you get their whole subtree.

Same verb, same query shape, opposite direction. It is the relation that says which.

```osy title="the chain of command above someone" test app=query-traverse
Employee[] ChainOfCommand(Employee e) {
  return Employee.Traverse(from: e, follow: x => x.Manager);   // an entity REF → walks up
}
```

### `where:` prunes, it does not filter   {#where-prunes}
This is the distinction that matters, and it is not the one people expect.

A `where:` predicate is applied **at each hop**, so a row that fails it is not just left out of the result — **it is
not walked through.** Everything beneath it is unreachable too. That is usually exactly what you want (an inactive
manager's whole branch is out of scope), and occasionally a surprise (you wanted the branch, minus that one row).

If you want to *filter* the result rather than prune the walk, traverse without a `where:` and filter what comes back.

### `depth:` is a fuse, not a target   {#depth}
`depth:` bounds how far the walk goes; it defaults to **10**. It is not a promise that the graph is that deep — it is
the thing that stops a walk running away. Combined with cycle detection (a row is never visited twice), a
self-referencing hierarchy fails safe rather than hanging.

Raise it when your hierarchy is genuinely deeper; do not remove it, because there is no removing it.

### Edges held by value: `match:` and `bidirectional:`   {#match}
Sometimes the graph is not a declared relation at all — it is an edge table holding two references (a "related
product", a "duplicate of", a follower graph). `match:` walks by **column value** rather than by relation:

- `follow:` names the column to leave by, `match:` names the column to arrive at.
- **`bidirectional: true`** walks the edge in both directions — the shape a symmetric relationship ("is related to")
  actually has, where an edge recorded one way should be found from either end.

### What it gives back   {#result}
A collection of the rows it reached — the same entity type you started from. It is not a chain: you cannot `Where` or
`OrderBy` a `Traverse`. Materialise it and work on the result.

**Entity-only.** There is nothing to traverse on a local [list](https://osysharp.com/reference/query/in-memory-linq/).

## Examples       {#examples}
A category tree, and why a hand-rolled loop is not the same thing:

```osy title="a category and every category beneath it" test app=query-traverse
entity Category {
  [Required, MaxLength(80)] string Name;
  Category Parent;
  [ForeignKey(Parent)] Category[] Children;
}

Category[] Subtree(Category root) {
  return Category.Traverse(from: root, follow: c => c.Children, depth: 6);
}

Category[] Roots(Category a, Category b) {
  return Category.Traverse(from: [a, b], follow: c => c.Parent);   // several seeds at once
}
```

Written by hand, the first one is a queue, a visited-set, a cycle check, and one query per level — and it is a query
per level that makes it slow on a deep tree, which is precisely the thing a recursive query avoids.

## See also       {#see-also}
- [Child collections (navigating a relation)](https://osysharp.com/reference/query/collections/) — a single hop: a parent's children
- [Include (pre-loading relations)](https://osysharp.com/reference/query/include/) — pre-loading a *known* depth of graph, rather than an unknown one
- [relations](https://osysharp.com/reference/entity/relations/) — declaring the self-relation a traverse walks
- [Querying data](https://osysharp.com/reference/query/index/) — where `Traverse` sits (it is not a chain verb)


---

<!-- https://osysharp.com/reference/query/set-operators/ -->

# Union / Concat / Intersect / Except

> Combines two row-queries over the same entity with SQL set semantics: Union dedups, Concat keeps duplicates (UNION ALL), Intersect keeps rows present in both sides, Except keeps left rows not in the right. One SQL statement over the entity.

<!-- id: query-set-operators · area: query · stability: stable · html: https://osysharp.com/reference/query/set-operators/ -->

## Summary        {#summary}
The LINQ set operators combine **two row-queries over the same entity** with SQL set semantics. `Union`
is set union (duplicates removed), `Concat` appends (duplicates kept — SQL `UNION ALL`), `Intersect`
keeps rows present in **both** sides, `Except` keeps left rows **not** in the right. The operands run as
one SQL statement — `(left SELECT) UNION [ALL] / INTERSECT / EXCEPT (right SELECT)`.

## Signature      {#signature}
```osy syntax
<rows>.Union(<rows>)      // set union — duplicates removed
<rows>.Concat(<rows>)     // append — duplicates kept (UNION ALL)
<rows>.Intersect(<rows>)  // rows in BOTH sides
<rows>.Except(<rows>)     // left rows NOT in the right
```
where `<rows>` is an entity set or an entity-row chain (`Where` / `OrderBy` / `Skip` / `Take` /
`Distinct`) — both sides the **same entity**.

## Description    {#description}
Semantics are exactly C# / EF:

| Operator | SQL | Duplicates |
|---|---|---|
| `Union` | `UNION` | removed |
| `Concat` | `UNION ALL` | kept — a row matching both sides appears twice |
| `Intersect` | `INTERSECT` | removed |
| `Except` | `EXCEPT` | removed |

- Entity rows dedup by their full column list — effectively **by Id** (rows are PK-unique), which is the
  C# result for object sequences.
- Each operand may carry its own `Where` / `OrderBy` / `Skip` / `Take` / `Distinct` chain. A paged
  operand is parenthesized in the SQL, so its `ORDER BY` / `LIMIT` stays scoped to that side.
- A **bare entity set** is a valid operand (the whole set): `Tag.Where(p).Union(Tag)`.
- **Read-your-own-writes:** uncommitted `new T{}` rows fold into the result per the op's semantics — a
  ghost matching either side joins a `Union`; one matching both sides appears twice in `Concat`, joins an
  `Intersect`, and is excluded by `Except`.
- **Durable:** the read memoizes like any query — a resume does not re-run it.

### Boundaries (pointed diagnostics)   {#boundaries}
- **Operands must be the same entity** — `Order.Union(Customer)` is a compile error (C# requires a
  common element type).
- **Rows only** — no `Select` projection on either side, and no `GroupBy`/`SelectMany`/`Join` before the
  operator.
- **One pair per expression** — `A.Union(B).Union(C)` is refused (`chained set operators are not
  supported yet`).
- **No clauses over the combined set** — `A.Union(B).Count()` / `.OrderBy(…)` refuse with
  `materialize with .ToList() first`.
- **No `Include`** on either side — apply includes after materializing.

## Examples       {#examples}
```osy title="all four operators" test app=query-set-ops
entity Tag {
  [MaxLength(50)] string Label;
  [MaxLength(10)] string Grp;
}

// Union — duplicates removed (a row matching both sides appears once).
Tag[] ActiveOrGroupB() {
  return Tag.Where(t => t.Label != "archived").Union(Tag.Where(t => t.Grp == "b")).ToList();
}

// Concat — duplicates kept (UNION ALL).
Tag[] Appended() { return Tag.Where(t => t.Grp == "a").Concat(Tag.Where(t => t.Grp == "b")).ToList(); }

// Intersect — rows present in BOTH sides.
Tag[] Both() { return Tag.Where(t => t.Label != "x").Intersect(Tag.Where(t => t.Grp == "b")).ToList(); }

// Except — left rows NOT in the right side.
Tag[] LeftOnly() { return Tag.Where(t => t.Label != "x").Except(Tag.Where(t => t.Grp == "b")).ToList(); }

// A bare entity set is a valid operand (the whole set).
Tag[] All() { return Tag.Where(t => t.Grp == "a").Union(Tag).ToList(); }
```

## See also       {#see-also}
- [Distinct](https://osysharp.com/reference/query/distinct/) — per-side and standalone row dedup
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — the operand chains
- [ToList](https://osysharp.com/reference/query/tolist/) — materializing the combined set
- [LINQ over a local list](https://osysharp.com/reference/query/in-memory-linq/) — the same set operators over a local `List`/`HashSet` (identical spellings)


---

<!-- https://osysharp.com/reference/query/update/ -->

# Update

> Ends a query chain with a set-based UPDATE: every row the chain selects gets the assignments applied, in the database, immediately — and the call answers how many rows were written. A value may read the row itself (`o.Balance - o.Fee`), so per-row arithmetic runs as SQL and increments compose under concurrency.

<!-- id: query-update · area: query · stability: stable · html: https://osysharp.com/reference/query/update/ -->

## Summary        {#summary}
`.Update(…)` ends a query chain the way [`.Delete()`](https://osysharp.com/reference/query/delete/) does — but instead of deleting the matching
rows it **writes** them: the body is a list of assignments to the row, applied set-based in one statement, at the
call site. The answer is how many rows were written. It is the same verb as C#'s `ExecuteUpdate`, under the natural
name and with the natural body — assignments, not a `SetProperty` chain.

## Signature      {#signature}
```osy syntax
<Entity>.Where(o => …).Update(o => { o.Status = "Closed"; })            →   int
<Entity>.Where(o => …).Update(o => { o.Total = o.Total + o.Fee; })      →   int   // the RHS reads the row
<Entity>.Where(o => …).OrderBy(k).Take(n).Update(o => { … })            →   int   // bounded — the chunked pass
```

## Description    {#description}

### Which rows does it write?   {#the-target-set}
Exactly the rows the chain would have returned, narrowed further by the entity's `allow update` rules — **per
assigned property**: a row is updatable only if every property you assign is granted for it, and a caller with no
grant at all for one of the assigned properties is refused naming it. Rows outside the set are untouched, never an
error; the count says how many were written.

```osy title="close every stale order" test app=query-update
entity Order {
  [Required] string Status;
  decimal Total;
  decimal Fee;
}

int CloseStale() {
  return Order.Where(o => o.Status == "Stale").Update(o => { o.Status = "Closed"; });
}
```

### A value can read the row   {#row-referencing}
An assignment's value may reference the row's own properties. It becomes part of the SQL, so each row computes with
**its own** values, and concurrent increments compose instead of losing writes:

```osy title="add each order's own fee to its total — one statement, per-row arithmetic" test app=query-update
int ApplyFees() {
  return Order.Where(o => o.Status == "Closed").Update(o => { o.Total = o.Total + o.Fee; });
}
```

A value may also be a captured local or parameter — it binds like a query predicate's would. It may read
**through** the row's references, and it may embed a **correlated scalar read** — an aggregate over the row's own
collection, or an entity-rooted one:

```osy title="hops and correlated aggregates as values — still one statement" test app=query-update
entity Region { [Required] string Name; decimal TaxRate; }
entity Account {
  [Required] string Status;
  Region? Region;
  decimal TaxRate;
  decimal Total;
  [ForeignKey(Account)] Entry[] Entries;
}
entity Entry { [Required] Account Account; decimal Amount; }

int Restamp() =>
  Account.Where(a => a.Status == "Open").Update(a => {
    a.TaxRate = a.Region.TaxRate ?? 0m;             // a hop — null when the reference is absent, so answer for it
    a.Total   = a.Entries.Sum(e => e.Amount);       // ITS OWN entries, correlated per row
  });
```

A hop through a reference that can be absent is null for rows without one — assigning that to a non-nullable
member refuses at compile until you answer for absence (`?? <fallback>`) or declare the member nullable, the same
standard an empty-set `Max(…)` holds. What a value may NOT be is a **row-returning** query: a set statement
assigns one scalar per row — aggregate it, or compute it into a local first.

### When does it run?   {#immediacy}
Immediately — at the call, not at `UnitOfWork.Commit()`, exactly like [`.Delete()`](https://osysharp.com/reference/query/delete/). It writes the
stored rows, so it refuses (naming the remedy) while your unit of work holds uncommitted changes of the same type.

### What about the entity's rules?   {#constraints}
They hold. An `[Immutable]` property, a workflow-owned state field or a platform-stamped field is refused **at
compile time**, naming the reason. Value rules (`[Min]`, `[Max]`, `[Pattern]`, `[MinLength]`, `[Required]`) and the
entity's `invariant`s are re-checked over the written rows **inside the same transaction** — one violating row
rolls the whole statement back, with the rule's own message:

```osy title="a guarded increment — one row over the limit rolls everything back" test app=query-update
entity Meter {
  [Required] string Zone;
  [Max(100)] int Load;
}

int Shed(int by) {
  return Meter.Where(m => m.Zone == "North").Update(m => { m.Load = m.Load + by; });
}
```

### A list in memory?   {#local-lists}
`.Update(…)` writes **database rows**. Elements of a local list change with a plain loop —
`foreach (var x in xs) { x.Prop = value; }` — and the compiler says so if you reach for the wrong verb.

## Examples       {#examples}
```osy title="a maintenance pass with a bound batch" test app=query-update
int ArchiveOldest() {
  return Order.Where(o => o.Status == "Closed").OrderBy(o => o.Total).Take(100)
              .Update(o => { o.Status = "Archived"; });
}
```

## See also       {#see-also}
- [Delete](https://osysharp.com/reference/query/delete/) — the delete terminal, same shape and same security story
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — saying which rows
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `allow update` rules that narrow the set, per assigned property


---

<!-- https://osysharp.com/reference/query/where/ -->

# Where / Single / Count

> Query an entity by writing a predicate over it. The query runs in the database — not a filter over rows you already fetched — so a table with millions of rows costs you only the ones you ask for.

<!-- id: query-where · area: query · stability: stable · html: https://osysharp.com/reference/query/where/ -->

## Summary        {#summary}
You query an entity by naming it and writing a predicate: `Order.Where(o => o.Total > 100m)`. The predicate is
**compiled into the database query** — it is not a filter applied to rows you already loaded — so a table with ten
million rows costs you the ones you asked for.

## Signature      {#signature}
```osy syntax
<Entity>.Where(<e> => <bool>)            // a set of rows — materialise with .ToList()
<Entity>.Single(<e> => <bool>)           // exactly one; faults if none, or if several
<Entity>.FirstOrDefault(<e> => <bool>)   // the first, or null
<Entity>.Count()                         // how many
<Entity>.Count(<e> => <bool>)            // how many match
<Entity>.Any(<e> => <bool>)              // is there at least one
```

## Description    {#description}

### `Where`, `Single`, `Count`, `Any` — which one?   {#choosing}
They differ in what they promise, and picking the wrong one is how a bug hides:

| Call | Returns | When there is no match | When there are several |
|---|---|---|---|
| `Where` | a set (materialise with [`.ToList()`](https://osysharp.com/reference/query/tolist/)) | an empty set | all of them |
| `Single` | one row | **faults** | **faults** |
| `FirstOrDefault` | one row or `null` | `null` | the first |
| `Count` | a number | `0` | the count |
| `Any` | a bool | `false` | `true` |

`Single` is a claim: *there is exactly one*. Use it when a second match would mean the data is broken — and be glad it
faults, because a `FirstOrDefault` there would quietly pick one and let the corruption spread.

```osy title="each one, doing its job" test app=query-where
entity Order {
  [Required] string Code;
  decimal Total;
  bool Cancelled;
}

Order ByCode(string code) {
  return Order.Single(o => o.Code == code);        // a code identifies exactly one order
}

Order LatestOrNull(decimal min) {
  return Order.FirstOrDefault(o => o.Total >= min); // there may be none — and that is fine
}

int BigOrders(decimal min) {
  return Order.Count(o => o.Total >= min && !o.Cancelled);
}

bool AnyCancelled() {
  return Order.Any(o => o.Cancelled);
}
```

### The predicate runs in the database   {#in-the-database}
`Order.Where(o => o.Total > 100m)` does not fetch every order and sift them. It becomes a `WHERE` clause. That is why
you should express the filter in the predicate rather than fetching and testing in a loop:

```osy title="filter in the query, not in the loop" test app=query-where
// GOOD — the database returns the rows you want
decimal BigTotal(decimal min) {
  var total = 0m;
  foreach (var o in Order.Where(o => o.Total >= min).ToList()) {
    total += o.Total;
  }
  return total;
}
```

Fetching everything and filtering in a `foreach` gives the same answer on your laptop with fifty rows, and takes the
application down when the table has five million.

### Dates and arithmetic go in the predicate too   {#dates}
A predicate is not limited to comparing columns to constants. Date arithmetic on a column — including a shift by
another COLUMN's value — becomes part of the `WHERE` clause, and so does the current time:

```osy title="a due-date predicate, evaluated in the database" test app=query-where-dates
entity Kiln {
  [Required, MaxLength(60)] string Name;
  [Required] int FireEveryDays;
  DateTime? LastFiredAt;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

// Never fired, or fired longer ago than its own interval.
Kiln[] Due() {
  return Kiln.Where(p => p.LastFiredAt == null
                      || p.LastFiredAt.Value.AddDays(p.FireEveryDays) < DateTime.UtcNow).ToList();
}
```

The alternative — `Kiln.ToList()` and then a LINQ filter over the result — reads almost the same and is a different
program: it fetches the whole table and does the work in memory. That is fine for the fifty rows you are testing with
and is the shape that stops scaling first.

## See also       {#see-also}
- [ToList](https://osysharp.com/reference/query/tolist/) — turning a `Where` into rows you can walk
- [Skip / Take (paging)](https://osysharp.com/reference/query/paging/) — `Skip` / `Take`
- [Distinct](https://osysharp.com/reference/query/distinct/) — removing duplicates
- [relations](https://osysharp.com/reference/entity/relations/) — why children come from a collection, not a filtered query


---

<!-- https://osysharp.com/reference/query/window/ -->

# Window

> Ranking and neighbours inside a query's result. The indexed `Select((s, i) => …)` over an ordered chain is C#'s own spelling of a row number; the `Window.*` functions add what C# has no spelling for — per-partition ranks, the previous/next row's value, running aggregates — all computed by the database in the same statement.

<!-- id: query-window · area: query · stability: stable · html: https://osysharp.com/reference/query/window/ -->

## Summary        {#summary}
A window function computes a value **about a row's place among the other rows** — its rank, the previous row's
value, a per-group aggregate — without collapsing the rows the way `GroupBy` does. Every row still comes back;
each carries its answer.

## Signature      {#signature}
```osy syntax
<ordered query>.Select((s, i) => …)                          // i = the 0-based position, C#'s own spelling
Window.RowNumber(orderBy: k)                                  → int
Window.Rank(orderBy: k, partitionBy: p)                       → int    // ties share, next rank skips
Window.DenseRank(orderBy: k, partitionBy: p)                  → int    // ties share, no gap
Window.Lag(value, orderBy: k, partitionBy: p)                 → T?     // the PREVIOUS row's value — null at the edge
Window.Lead(value, orderBy: k, partitionBy: p)                → T?     // the NEXT row's value — null at the edge
Window.Sum(value, orderBy: k, partitionBy: p) · Avg · Min · Max · Count
Window.Sum(value, orderBy: k, rowsBefore: n, rowsAfter: m)   // a sliding frame — aggregators only
```
`orderByDescending:` orders the window the other way. `partitionBy:` is optional — without it the window is the
whole result; `partitionBy: new { a, b }` partitions on the pair. `rowsBefore:`/`rowsAfter:` bound an aggregate
to the rows around the current one; without them an ordered aggregate is a running total. `Window.*` lives
**inside a `.Select(…)` projection** over stored rows, nowhere else.

## Description    {#description}

### How do I number the rows?   {#row-number}
With the index C# already gives a `Select` — legal over stored rows once the chain names an order (a table has no
position until you say which):

```osy title="a leaderboard, numbered in points order" test app=query-window
entity Score {
  [Required] string Player;
  [Required] string Region;
  [Required] string Tier;
  int Points;
}

string Board() {
  var rows = Score.OrderByDescending(s => s.Points)
                  .Select((s, i) => new { Line = (i + 1) + ". " + s.Player });
  var outText = "";
  foreach (var r in rows) { outText = outText + r.Line + "\n"; }
  return outText;
}
```

Without the `OrderBy` this refuses at compile, with the order as the remedy.

### How do I rank within groups?   {#partitioned}
`partitionBy:` restarts the window per group — every region gets its own ranking, in one statement:

```osy title="per-region ranks, every row still a row" test app=query-window
string RegionBoards() {
  var rows = Score.OrderBy(s => s.Region)
                  .Select(s => new {
                    s.Player, s.Region,
                    Rank = Window.Rank(orderByDescending: s.Points, partitionBy: s.Region),
                  });
  var outText = "";
  foreach (var r in rows) { outText = outText + r.Region + ":" + r.Player + "#" + r.Rank + "\n"; }
  return outText;
}
```

`Rank` gives ties the same number and skips the next (1, 1, 3); `DenseRank` doesn't skip (1, 1, 2);
`RowNumber` never ties.

A group keyed by **more than one column** is an anonymous object — the same spelling `GroupBy` uses for a
composite key. The window restarts wherever any part of the pair changes:

```osy title="ranks within each region AND tier" test app=query-window
string TierBoards() {
  var rows = Score.OrderBy(s => s.Region).ThenBy(s => s.Tier)
                  .Select(s => new {
                    s.Player, s.Region, s.Tier,
                    Rank = Window.Rank(orderByDescending: s.Points, partitionBy: new { s.Region, s.Tier }),
                  });
  var outText = "";
  foreach (var r in rows) { outText = outText + r.Region + "/" + r.Tier + ":" + r.Player + "#" + r.Rank + "\n"; }
  return outText;
}
```

### How do I sum a sliding window?   {#frame}
An aggregate with an `orderBy:` is a **running** total by default — every row sums itself and everything before
it. `rowsBefore:` and `rowsAfter:` narrow that to the rows around the current one, counted in the window's order:
`rowsBefore: 2` is this row and the two before it; `rowsAfter: 1` is this row and the next; both together is a
centred frame. A bound counts ROWS, not values — three rows with equal points are still three rows.

```osy title="a three-row moving average, and what is still to come" test app=query-window
string Trend() {
  var rows = Score.OrderBy(s => s.Points)
                  .Select(s => new {
                    s.Player,
                    Running = Window.Sum(s.Points, orderBy: s.Points),
                    Around  = Window.Avg(s.Points, orderBy: s.Points, rowsBefore: 1, rowsAfter: 1),
                    Ahead   = Window.Count(orderBy: s.Points, rowsAfter: 2),
                  });
  var outText = "";
  foreach (var r in rows) { outText = outText + r.Player + ":" + r.Running + "/" + r.Around + "/" + r.Ahead + "\n"; }
  return outText;
}
```

A frame belongs to the aggregators — `Sum`, `Avg`, `Min`, `Max`, `Count`. A rank is over the whole partition
and `Lag`/`Lead` reach a fixed distance already, so a bound on any of those refuses at compile; so does a frame
with no `orderBy:` (a slice of an unordered set means nothing) and a negative bound.

### How do I read the previous row?   {#lag}
`Lag` (and `Lead`) hand you a neighbouring row's value. At the window's edge there is no neighbour, so the answer
is **null** — the type says so, and you answer for it like any other absence:

```osy title="the gap to the previous score" test app=query-window
string Gaps() {
  var rows = Score.OrderBy(s => s.Points)
                  .Select(s => new {
                    s.Player,
                    Gap = s.Points - (Window.Lag(s.Points, orderBy: s.Points) ?? s.Points),
                  });
  var outText = "";
  foreach (var r in rows) { outText = outText + r.Player + ":" + r.Gap + "\n"; }
  return outText;
}
```

### Where may a window stand?   {#position}
Only inside a `.Select(…)` projection over stored rows — a window ranks a SET the database holds. Anywhere else it
refuses at compile, pointing here; over a local list, C#'s own tools (`Select((x, i) => …)` on the list, sorting,
indexing) already answer.

## See also       {#see-also}
- [Select (projections)](https://osysharp.com/reference/query/select/) — the projection a window lives in
- [OrderBy / ThenBy](https://osysharp.com/reference/query/ordering/) — the order a window ranks by


---

<!-- https://osysharp.com/reference/realtime/presence/ -->

# Here, Announce

> Who is currently on a presence topic, and what they say they are doing. Here is the converging set of people present, derived from their connections rather than written by the app; Announce decorates your own entry in that set and can never create or modify anybody else's, because the platform stamps who each entry is about.

<!-- id: realtime-presence · area: realtime · stability: stable · html: https://osysharp.com/reference/realtime/presence/ -->

## Summary        {#summary}

`Here` is who is **currently connected** to a presence topic; `Announce` says what you are doing. Both are read and
written on a page, and neither needs a row: the set is derived from the connections themselves.

```osy title="who is here, and what they are doing" test app=realtime-presence
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Room {
  [Required, MaxLength(120)] string Name;
  security { allow read, create when IsAuthenticated; }
}

entity Member {
  [Required] Room Room;
  [Required] User Person;
  security {
    allow read where Member.Any(m => m.Room == Room && m.Person == user);
    allow create where Person == user;
  }
}

class Presence {
  public User Person;        // STAMPED by the platform — who this entry is about
  public string Activity;    // announced by that person
}

topic RoomPresence(Guid roomId) {
  Candidates = u => Member.Any(m => m.Room.Id == roomId && m.Person == u);
  Carries    = Presence;
  Presence   = true;
}

[Page("/room/{roomId}/header")]
[Render(CSR)]
component RoomHeader(Guid roomId) {
  live var here = RoomPresence.For(roomId).Here;

  on mount {
    RoomPresence.For(roomId).Announce(new Presence { Activity = "reading" });
  }

  render {
    Text($"{here.Count} here");
    foreach (var p in here) { Text($"{p.Person.Email} — {p.Activity}"); }
  }
}
```

## Signature      {#signature}

```osy syntax
live var here = Topic.For(<address>).Here;       // the set of people currently present
Topic.For(<address>).Announce(<status>);         // decorate your OWN entry; never somebody else's
```

## Description    {#description}

### `Here` is a set, not a feed   {#here}

A presence topic's `Here` is the converging set of who is currently present — not a log of arrivals and departures.
Reading it as a set is what makes it self-correcting: a page that misses one update is fixed by the next, rather than
drifting further from the truth with every missed event.

One person is one entry however many pages they have open.

### Presence is derived from the connection   {#derived}

You are in the set because your page is subscribed, and you leave because it went away. Nothing an app writes can put
somebody in a room they are not in, or keep them there after they have gone — which is what makes the set worth
trusting, and what stops a crashed browser staying online for ever.

Invisible mode needs no flag: a page that does not subscribe is not in the set.

### An entry is your own type, with WHO filled in for you   {#entry}

`Carries` names the class an entry is made of. Declare one field of your app's principal type on it — call it
whatever you like — and **the platform fills that field in**. Everything else on the class is yours to announce.

```osy syntax
class Presence {
  public User Person;        // the platform writes this; an app that tries is refused at compile time
  public string Activity;    // yours
}
```

The field is chosen by its **type**, not its name. A class with two principal-typed fields is ambiguous — the
compiler cannot tell which one means *who this entry is about* — and is refused at the call site naming both.

### `Announce` decorates your own entry, and only your own   {#announce}

`Announce` adds your detail to that entry — a status, an activity. It **cannot create or modify anybody else's**, and
that is structural rather than checked: the entry exists because your page is subscribed, and who it is about comes
from your connection. There is nothing in an `Announce` that names a person, so announcing as somebody else is not a
refused request — it is one you cannot write.

Presence vocabulary like *away* or *do not disturb* is the app's to define, not the platform's.

### You see the people you may see   {#visibility}

A presence set contains real records, so it obeys the same read rules everything else does: each viewer's set is
built under their own authority. Somebody present whose record you may not read is simply not in your set — the same
answer a query would give you, not a blank silhouette.

### Announcing is not a message   {#not-a-message}

An announcement is a statement about your **current** state, not an event. Re-announcing replaces; it does not
accumulate, and nobody receives a history of what you were doing. One person is one entry however many pages they
have open, and the most recent thing they said is what everyone sees.

## Examples       {#examples}

The example above is compiled by the documentation gate.

For what people SAY in a room rather than who is in it, see [Listen](https://osysharp.com/reference/realtime/listen/).

## See also       {#see-also}
- [Listen](https://osysharp.com/reference/realtime/listen/) — receiving what was SAID in a room, rather than who is in it
- [topic](https://osysharp.com/reference/realtime/topic/) — the declaration, `Presence = true`, and publishing
- [Realtime](https://osysharp.com/reference/realtime/index/) — realtime in one page
- [component](https://osysharp.com/reference/ui/component/) — the `live var` a presence set feeds


---

<!-- https://osysharp.com/reference/realtime/listen/ -->

# Listen

> The receiving half of a topic, consumed on a page. Listen yields a stream of the topic's payloads, and a live var bound to it appends each message as it arrives — no re-query, no polling, and no attribute to opt a page in. The subscription opens when the component mounts and is released when it unmounts.

<!-- id: realtime-listen · area: realtime · stability: stable · html: https://osysharp.com/reference/realtime/listen/ -->

## Summary        {#summary}

`Listen()` is how a page **receives** from a [topic](https://osysharp.com/reference/realtime/topic/). It yields a `stream<T>` of the topic's payloads;
a `live var` bound to it holds each message as it arrives.

```osy title="a live feed of a room" test app=realtime-listen
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Room {
  [Required, MaxLength(120)] string Name;
  security { allow read, create when IsAuthenticated; }
}

entity Member {
  [Required] Room Room;
  [Required] User Person;
  security {
    allow read where Member.Any(m => m.Room == Room && m.Person == user);
    allow create where Person == user;
  }
}

class ChatLine { public string Body; }

topic RoomFeed(Guid roomId) {
  Candidates = u => Member.Any(m => m.Room.Id == roomId && m.Person == u);
  Carries    = ChatLine;
}

[Page("/room/{roomId}")]
[Render(CSR)]
component RoomPage(Guid roomId) {
  live var lines = RoomFeed.For(roomId).Listen();      // stream<ChatLine> — appends as they arrive

  render {
    foreach (var line in lines) { Text(line.Body); }
  }
}
```

## Signature      {#signature}

```osy syntax
live var lines = Topic.For(<address>).Listen();   // stream<T>, appended to as messages arrive
```

## Description    {#description}

### There is nothing to opt in to   {#no-opt-in}

A page participates in realtime by **writing a subscription**, and by nothing else. There is no attribute, no shell
setting and no registration step: the `live var` above *is* the declaration, in the same way that declaring security on
an entity is the whole of declaring it.

### `Listen` yields a stream, deliberately   {#stream}

`Listen()` returns a `stream<T>` — the same thing a `live var` already consumes from a streaming function. That is the
point of the choice: a topic subscription needs no new rendering machinery, no new reconciler and no new `foreach`
path, because it is the mechanism the UI already has, with a topic as its producer instead of a function.

It also means the ordinary rules apply. The subscription is opened when the component mounts and released when it
unmounts; a reconnect re-establishes it; and the same page open twice on one topic is one subscription, not two.

### A stream is observed, never awaited   {#not-awaitable}

`Listen()` is legal in exactly one place: as the whole initializer of a `live var`. There is no single value to hold —
the messages are still arriving — so `var x = Topic.For(a).Listen()`, `await Topic.For(a).Listen()` and
`live var n = Topic.For(a).Listen().Count` are all refused, and say so.

### What you are holding is a list — query it like one   {#querying-a-subscription}

The `live var` a subscription is bound to is a collection that grows, so read it with the collection vocabulary you
already have. `foreach` renders it, `.Count` says how many have arrived, and ordinary LINQ answers the rest:

```osy syntax
live var nudges = Inbox.For(personId).Listen();

// …in the render:
foreach (var n in nudges.Reverse().Take(nudges.Count - dismissed)) { … }   // newest first, undismissed only
if (nudges.Any(n => n.Kind == NudgeKind.Mention)) { … }
```

Nothing re-reads anything to answer these — the messages are already in hand, and the chain runs where they are.
`.Done`, `.Failed`, `.Interrupted` and `.Error` are the reads that are *not* about the items: whether the producer
finished, and whether it stopped early. See [component](https://osysharp.com/reference/ui/component/).

⚠ The one thing you may not do is query a subscription you have not bound. `Topic.For(a).Listen().Where(…)` is
refused for the same reason `await` on it is: there is no list yet, only a producer. Bind it to a `live var` first.

### The address is checked at the call site — and followed while the page lives   {#address}

A topic's parameters are its address, so `RoomFeed.For(a)` and `RoomFeed.For(b)` are two subscriptions. The address binds
through the same argument rules as every other call in the language, and it is checked where you wrote it — because the
only runtime symptom of a wrong address is a page that never updates, which is indistinguishable from a quiet room.

**The subscription follows the address.** If what the address reads changes — a state member, a parameter — the
subscription is released and re-opened at the new one, and the stream is emptied first: what was said in the room you
left is not part of the room you joined.

```osy syntax
string room = "general";
live var lines = RoomFeed.For(room).Listen();   // switching `room` moves the subscription
```

**A refused join is re-asked when data changes.** `Candidates` is a question about your data, so a write is the only
thing that can change its answer — and after one, every refused subscription asks again. A page that opens a room the
reader may not enter yet therefore starts working the moment they are let in, with nothing to write for it.

⚠ What that does **not** do is re-run a plain query. A `var` read is a snapshot taken at mount by design, so a page
that also shows history has to decide for itself whether entering is worth re-reading it.

### What arrives is the value, not a signal   {#payload}

Each message carries the publisher's payload, and the page renders it directly. It does **not** re-read the database:
the subscription was admitted one principal at a time by the topic's `Candidates` rule, which is what makes carrying
the value sound. That is the whole economic argument for topics — a room of ten people, a hundred messages deep, costs
one message per viewer instead of a full re-query each.

### A topic that carries nothing cannot be listened to   {#signal-only}

`Carries` is what a subscriber binds. A signal-only topic has no value to deliver, so `Listen()` on one is a compile
error naming the missing declaration rather than a subscription that delivers empty frames.

## Examples       {#examples}

The example above is compiled by the documentation gate.

For who is *present* on a topic rather than what was said on it, see [Here, Announce](https://osysharp.com/reference/realtime/presence/).

## See also       {#see-also}
- [topic](https://osysharp.com/reference/realtime/topic/) — the declaration, and publishing
- [Here, Announce](https://osysharp.com/reference/realtime/presence/) — who is here, and what they are doing
- [Realtime](https://osysharp.com/reference/realtime/index/) — realtime in one page
- [component](https://osysharp.com/reference/ui/component/) — the `live var` a subscription feeds


---

<!-- https://osysharp.com/reference/realtime/index/ -->

# Realtime

> Realtime in Osy# is one construct: a topic. A topic is a declared, addressable destination — publishing to one delivers the value to every open page entitled to it, with no polling and no re-query. Its parameters are its address, its Candidates rule says who may join, and a topic marked Presence tracks who is currently there, derived from the connections themselves.

<!-- id: realtime-index · area: realtime · stability: stable · html: https://osysharp.com/reference/realtime/index/ -->

## Summary        {#summary}

Realtime is **one construct**: a [topic](https://osysharp.com/reference/realtime/topic/). Everything else — rooms, threads, direct messages, typing
indicators, read receipts — is ordinary app modelling on top of it.

```osy title="the smallest complete topic" test app=realtime-smallest
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

class Tick { public int Count; }

topic Heartbeat() {
  Candidates = _ => true;        // who may subscribe. REQUIRED — silence is a compile error
  Carries    = Tick;             // what each message holds
}

void Beat(int n) {
  Heartbeat.Publish(new Tick { Count = n });
}
```

## Description    {#description}

### What a topic is for   {#purpose}

A page that must reflect something happening elsewhere has three options, and only one of them scales:

| approach | what it costs |
|---|---|
| poll | every page re-asks on a timer, whether or not anything changed |
| signal, then re-read | one message tells the page *something* changed and it re-runs its whole query |
| **a topic** | the message carries the value, so the page receives exactly what changed |

The middle option is what a plain live query does, and it is correct but expensive: a signal carrying no value can
only be answered by re-asking the whole question. Measured, a room of ten people a hundred messages deep moves about
a thousand rows to deliver one "ok" — every one of which was already on every screen. The same message on a
value-carrying topic moves ten.

### Security is declared, once   {#security}

A topic's `Candidates` rule decides who may subscribe, and the platform enforces it at the moment of joining. There
is nothing to remember at the call site and nothing an app can bypass, which is the same split entity security
already has: the app owns the policy, the platform owns the enforcement.

Because the address is made of typed parameters rather than a string name, **a topic's name is never its security
boundary** — guessing `RoomFeed` gets you nothing without a rule that admits you at that address.

### What is not part of it   {#not-included}

A topic is transport. The vocabulary of a chat product — channels, threads, DMs, reactions, read receipts,
moderation — is app modelling, and the platform deliberately ships none of it: those are entities and rules an app
declares, and they are better for being the app's own.

## Examples       {#examples}

See [topic](https://osysharp.com/reference/realtime/topic/) for compiled examples: a room feed with a membership rule, an explicitly public topic, a
broadcast topic with a separate publish rule, a webhook publishing with no row to carry it, and presence.

## See also       {#see-also}
- [topic](https://osysharp.com/reference/realtime/topic/) — the construct, in full
- [Listen](https://osysharp.com/reference/realtime/listen/) — the subscribe half: what a page receives
- [Here, Announce](https://osysharp.com/reference/realtime/presence/) — who is here, and what they are doing
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `allow` rules a `Candidates` predicate reads
- [component](https://osysharp.com/reference/ui/component/) — where a subscription is consumed


---

<!-- https://osysharp.com/reference/realtime/topic/ -->

# topic

> A topic is a named, addressable realtime destination: a declared place to put a message so that every open page entitled to it receives the value, without re-reading the database. Its parameters are its address, so RoomFeed.For(a) and RoomFeed.For(b) are two topics. Candidates says who may subscribe and is required. Publishing takes no commit and no unit of work.

<!-- id: realtime-topic · area: realtime · stability: stable · html: https://osysharp.com/reference/realtime/topic/ -->

## Summary        {#summary}

A **topic** is a declared destination for messages. Publishing to one delivers the value to every open page that is
entitled to it — no polling, no re-query, and no row has to exist to carry the message.

```osy title="a room feed" test app=realtime-room
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Room {
  [Required, MaxLength(120)] string Name;
  security { allow read, create when IsAuthenticated; }
}

entity Member {
  [Required] Room Room;
  [Required] User Person;
  security {
    allow read where Member.Any(m => m.Room == Room && m.Person == user);
    allow create where Person == user;
  }
}

class ChatLine { public string Body; }

topic RoomFeed(Guid roomId) {
  Candidates = u => Member.Any(m => m.Room.Id == roomId && m.Person == u);
  Carries    = ChatLine;
}

void Say(Guid roomId, string body) {
  RoomFeed.For(roomId).Publish(new ChatLine { Body = body });
}
```

## Signature      {#signature}

```osy syntax
topic Name(<address params>) {
  Candidates = u => <predicate>;   // REQUIRED — who may subscribe
  Publishers = u => <predicate>;   // omitted ⇒ the same as Candidates
  Carries    = <type>;             // omitted ⇒ a signal topic, carrying no value
  Delivery   = Ephemeral;          // the only value today. Omitted ⇒ Ephemeral
  Presence   = true;               // omitted ⇒ false
}

Name.For(<address>).Publish(<payload>);   // from a server function, an action, or an api endpoint
```

This fence is a TEMPLATE — `<address params>` is a placeholder, not something that compiles — so it is illustrative
rather than compiled, and every claim it makes is backed by a compiled example further down: the declaration form by
[the room feed above](#summary), and each of the three publishing positions by [`Publish`](#publish).


## Description    {#description}

### The parameters are the address, not the name   {#address}

`RoomFeed.For(roomA)` and `RoomFeed.For(roomB)` are two different topics. This is the reason topics are declared rather than
named by string: an address made of typed parameters is one the compiler checks, and one a security rule can read.

It also means **a topic's name is never its security boundary**. Knowing the name `RoomFeed` gets you nothing; what
decides whether you receive a room's traffic is `Candidates`, evaluated for you, at that address.

Two spellings of one address are one address — an upper-case and a lower-case spelling of the same `Guid` reach the
same subscribers, because the address is compared by its bound values rather than by the text that arrived.

### `Candidates` — who may subscribe, and it is required   {#candidates}

`Candidates` is a predicate over the principal, evaluated at subscribe time with the topic's address in scope. It is
free to read app data, which is what lets a *user* of the app configure who may join — a membership table, a role
grant — without the developer recompiling.

It is deliberately the same spelling a workflow slot's `Candidates` uses, because it is the same question.

**A topic with no `Candidates` is a compile error.** Entity security takes the opposite posture — say nothing and
nobody may — and that is right there, because forgetting a rule fails loudly: nobody can read the table and it is
reported within the minute. A topic nobody may join fails the other way. The page simply never updates. There is no
error, no refusal, nothing in a log, and it looks exactly like a quiet room. It is the least reportable failure in
the system, so it is the one that must not be silent.

A deliberately public topic says so out loud:

```osy title="an explicitly public topic" test app=realtime-public
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

class Tick { public int Count; }

topic Heartbeat() {
  Candidates = _ => true;      // loud, greppable, and a decision somebody made
  Carries    = Tick;
}
```

### `Publishers` — who may publish, defaulting to `Candidates`   {#publishers}

Omitting `Publishers` means *the same people who may listen may speak*, which is right for nearly every topic. The
two cases that differ are both real and both one line: a broadcast topic (many subscribe, a server function
publishes) and a firehose (many publish, few subscribe).

```osy title="a broadcast topic — anyone may listen, only staff may speak" test app=realtime-broadcast
[Principal] entity User {
  [MaxLength(200)] string Email;
  bool IsStaff;
  security { allow read when IsAuthenticated; }
}

class Notice { public string Text; }

topic Announcements() {
  Candidates = _ => true;
  Publishers = u => u.IsStaff;
  Carries    = Notice;
}
```

**The publisher is stamped by the platform.** Who sent a message is taken from the acting principal, never from the
payload — so a message claiming to be from somebody else is not expressible rather than merely refused. You do not
have to remember to set it, and you cannot get it wrong.

⚠ Write the rule in the declaration, not in the function that publishes. A publish rule written in a function body is
enforced only on the path that remembers to call it, and it is reliably weaker than the one you would have declared:
the natural hand-written check asks whether the caller is *in the room* and forgets to ask whether the message names
them as its author.

### A gate reads COMMITTED data — so commit before you publish   {#gates-read-committed-data}

`Candidates` and `Publishers` are evaluated against the database **as it is committed**, not as your unit of work
currently has it. That is deliberate and it matches every other gate in the platform: authorization is decided
against the data that exists, never against a caller's pending, uncommitted view of it.

It has one consequence worth knowing before you meet it. If a single action *creates the row that authorizes the
publish* and then publishes, the publish is refused — because at the moment the gate runs, that row is still pending:

```osy title="✗ publishing on a membership that is still pending" syntax
void JoinAndGreet(Channel channel, string body) {
  new Membership { Channel = channel, Person = Session.CurrentUser };   // not committed yet…
  ChannelFeed.For(channel.Id).Publish(new PostLine { Body = body });    // …so `Candidates` cannot see it → refused
}
```

Commit first, and it behaves as you would expect:

```osy title="commit the authorizing row first, then publish" syntax
void JoinAndGreet(Channel channel, string body) {
  new Membership { Channel = channel, Person = Session.CurrentUser };
  UnitOfWork.Commit();                                                  // the membership is now a fact
  ChannelFeed.For(channel.Id).Publish(new PostLine { Body = body });    // `Candidates` sees it
}
```

The refusal names your own rule, which reads like a bug in the rule rather than a question of ordering — so when a
publish is refused by a rule you believe should pass, check what the current unit of work is still holding. It will
tell you: the refusal lists the entities your unit of work has pending, so you can see the row that was going to
authorize you sitting there uncommitted.

#### A page action is one unit of work, and so is a `[Test]` body   {#one-unit-of-work}

This catches people because a **page action does not commit either.** Calling a server function from a page
carries your uncommitted edits along with the call — the server runs your query against them, so you read your own
changes — but nothing is written until an explicit `UnitOfWork.Commit()`. So the refusal above is not something that
only happens in an unusual place: it is what a page doing both halves in one action gets.

A `[Test]` body behaves **identically**, and deliberately so — it is modelling that page action. If your test creates
the authorizing row and then subscribes or publishes, commit in between, exactly as the app itself would have to:

```osy title="the same commit, in a test body" syntax
var channel = CreateChannel("design", "");   // stages a Channel and a Membership
UnitOfWork.Commit();                         // …now they are facts the gate can see
ChannelFeed.For(channel.Id).Listen();        // admitted
```

### `Publish` — no commit, no unit of work   {#publish}

Publishing is not a data write. It opens no transaction and needs no row, which is what lets an inbound webhook or
an agent mid-answer reach a page directly.

#### Where you may publish from   {#publish-positions}

From a server function, from an `api` endpoint, and **from a page `action`** — the three positions the signature
names, each with a compiled example here.

A publish written straight into an action is the same publish. You do not have to route it through a server
function to be allowed to write it:

```osy title="publishing from a page action" test app=realtime-action-publish
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

class Line { public string Body; }

topic Lobby(string room) {
  Candidates = _ => true;
  Carries    = Line;
}

[Page("/lobby")]
[Render(CSR)]
[AllowAnonymous]
component LobbyPage() {
  string draft = "";

  action Send() {
    Lobby.For("main").Publish(new Line { Body = draft });
    draft = "";
  }

  render {
    Stack(gap: 2) {
      Input(value: draft, label: "Message");
      Osysharp.Button("Send", onClick: Send);
    }
  }
}
```

**It still runs on the server, and that is not an implementation detail you can lose.** The browser evaluates the
address and the payload, then hands the publish to the host, which performs it under its own authority: `Publishers`
is evaluated there, and the sender is stamped there from the connection. So the rules in this page hold identically
whether you publish from an action or from a server function — including the committed-data rule below, because
the action's own uncommitted edits are not facts yet when the gate reads them.

That also means a publish is the one thing in an action that **cannot** be made to lie about who sent it. There is no
argument for the sender anywhere in this surface, and the browser is given nothing to say about identity.

```osy title="publishing from an api endpoint" test app=realtime-webhook
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

class Delivery { public string Status; }

topic OrderTracking(Guid orderId) {
  Candidates = _ => true;
  Carries    = Delivery;
}

void CarrierUpdate(Guid orderId, string status) {
  OrderTracking.For(orderId).Publish(new Delivery { Status = status });
}
```

A refused publish **throws**, like any other refusal of a declared rule. It does not return quietly: a message
nobody received is the hardest failure in this system to notice, so it is not one of the silent ones.

### `Carries` — the payload type, or a signal   {#carries}

`Carries` names what each message holds — an app `class`. A topic that declares none is a **signal**: `Publish()`
takes no argument, and subscribers learn only that something happened.

**A published message may not carry a ROW**, and the refusal says so. A publish is composed once and goes to every
subscriber the join gate admitted, so a row inside it would be one person's view of that row handed to all of them —
and what a reader may see of a row is that reader's own question. Carry the values the readers need instead: a name,
an id, the two fields the line renders. (A **presence** entry is the exception that proves it: it carries the
principal's row, and the platform projects it separately for each recipient, through each one's own read rules.)

Prefer carrying the value. A signal can only be answered by re-asking the whole question, and that is what a topic
exists to avoid: a room of ten people a hundred messages deep costs about a thousand rows to deliver one "ok" when
the message carries nothing, and ten when it does.

### `Delivery` — `Ephemeral`, and only that   {#delivery}

`Ephemeral` (the default) is not persisted, and deliberately does **not** reach someone who was offline — a typing
indicator has no meaning to somebody who was not there.

`Delivery = Durable` is **refused at compile time**: it is not delivered yet, and accepting it would mean a subscriber
who was away silently received nothing.

**Write the durable half as rows and the instant half as the topic.** They are different in kind, and an app that
needs both wants both anyway:

- the **row** is what survives a reload and answers "what was said before I arrived";
- the **publish** carries the same values, so an open page renders them without re-reading anything.

That is two lines in the function that does the work — one `new`, one `Publish` — and it is what every app here does.

### `Presence` — the set of who is here   {#presence}

`Presence = true` makes the topic's membership **derived from who is connected to it**. The platform adds an entry
when a page subscribes and removes it when that page goes away, so a crashed browser cannot stay online for ever —
and no app code maintains it.

```osy title="who is in the room" test app=realtime-presence
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Room {
  [Required, MaxLength(120)] string Name;
  security { allow read, create when IsAuthenticated; }
}

entity Member {
  [Required] Room Room;
  [Required] User Person;
  security {
    allow read where Member.Any(m => m.Room == Room && m.Person == user);
    allow create where Person == user;
  }
}

topic RoomPresence(Guid roomId) {
  Candidates = u => Member.Any(m => m.Room.Id == roomId && m.Person == u);
  Presence   = true;
}
```

An app can never write somebody else's entry — only decorate its own. **Invisible mode is therefore free and needs
no setting: a page that does not subscribe is not in the set.**

Presence is gated by the same `Candidates` rule as any other join. That matters more than it looks: a presence set
names people, so admitting a stranger to "just the presence" of a private room would disclose its membership without
ever showing them a message.

One person is one entry however many pages they have open — a phone and a desktop are one person in the room.

### Subscribing from a page   {#subscribing}

A page receives with `Listen()` — see [Listen](https://osysharp.com/reference/realtime/listen/). On a `Presence = true` topic it also reads the SET of who
is connected, and announces its own state: see [Here, Announce](https://osysharp.com/reference/realtime/presence/).

## Examples       {#examples}

The compiled examples above are the reference set: a room feed with a membership rule, an explicitly public topic, a
broadcast topic with a separate `Publishers` rule, a page action publishing directly, a webhook publishing with no
row to carry it, and a presence topic.

## See also       {#see-also}
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `allow` rules a topic's `Candidates` predicate reads
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — the `Candidates` spelling a topic deliberately shares
- [Listen](https://osysharp.com/reference/realtime/listen/) — the subscribe half of the surface
- [Here, Announce](https://osysharp.com/reference/realtime/presence/) — who is here, and what they are doing
- [component](https://osysharp.com/reference/ui/component/) — where a subscription is consumed, as a `live var`


---

<!-- https://osysharp.com/reference/scheduling/schedule/ -->

# Schedule (recurring work)

> A schedule produces work on a cadence. Each occurrence creates a row of the entity its Template names — carrying whatever context the template sets — and a workflow that tracks that entity then starts on its own. Schedules are DATA, not syntax, so a cadence changes without a deploy.

<!-- id: scheduling-schedule · area: scheduling · stability: preview · html: https://osysharp.com/reference/scheduling/schedule/ -->

## Summary        {#summary}
A **`Schedule`** produces work on a recurring cadence. Each occurrence **creates a row of the schedule's
`Template`** — and that is the whole of what it does. A workflow whose [Tracks and Initial (the field a workflow drives)](https://osysharp.com/reference/workflow/tracks/) names the same entity
then starts through the ordinary autostart path, so a schedule never mentions a workflow and needs no new wiring to
reach one.

Schedules are **rows, not syntax**. Moving a nightly job from 02:00 to 03:00 is an edit, not a deploy; one workflow
can have several schedules; each tenant can have its own in its own zone; and a schedule can be paused, resumed and
retired without touching the source.

## Signature      {#signature}
```osy syntax
new Osysharp.Scheduling.Schedule {
  Name           = <string>,          // how operators refer to it
  Template       = new <Entity> { … },// each occurrence creates a row of THIS, with these members set
  Zone           = <"IANA zone">,     // the zone every local time below is measured in
  EffectiveFrom  = <DateTime>,        // when it starts — and its PHASE ORIGIN
  EffectiveUntil = <DateTime?>,       // optional end, inclusive of the instant
  Status         = Active | Paused | Retired,
  Overlap        = Skip | Allow
}
new Osysharp.Scheduling.ScheduleRule { Schedule = <schedule>, Every = <frequency>, Interval = <int> }
new ScheduleRuleTime     { Rule = <rule>, At    = <TimeSpan> }   // local time of day
new ScheduleRuleWeekday  { Rule = <rule>, Day   = <DayOfWeek> }
new Osysharp.Scheduling.ScheduleRuleMonthDay { Rule = <rule>, Day   = <int> }        // negative counts from the end: -1 is the last day
new ScheduleRuleMonth    { Rule = <rule>, Month = <Month> }
new Osysharp.Scheduling.ScheduleExclusion { Schedule = <schedule>, From = <DateTime>, To = <DateTime>, Reason = <string> }
```

## Description    {#description}

### What an occurrence does   {#occurrence}
When a schedule comes due the platform creates **one row of the entity its `Template` names** and re-arms the schedule for its next
occurrence. It does not start a workflow, call a function, or run any code you wrote. Everything after the row is the
platform's ordinary behaviour: a workflow that tracks that entity and autostarts will start, exactly as it would for a
row created by a person.

That is why a **scheduled workflow must be autostart-able on its tracked entity.** It is a real constraint and it is
the honest one — the schedule produces rows and the workflow reacts to rows.

### Making a schedule about a particular row — `Template`     {#template-members}
`Template` is an object initializer, so it sets members on the row each occurrence creates. That is the difference
between a schedule that is only a cadence and one that is *about* something — "chase **this** order every morning"
rather than "run every morning":

```osy syntax
Template = new ChaseTask { Order = order, Attempt = 1 },
```

Everything in that line is an **identifier**, checked when you compile: the entity, and every member of it. A
mistyped member is an error at the line that made it, not a surprise on the night the job first runs.

Two things worth knowing about the values:

- **A reference is stored as the row it points at**, so `Order = order` on the occurrence points at that same order.
- **They are captured when the schedule is created, not re-evaluated per occurrence.** A schedule is a row, so
  `Attempt = 1` means every occurrence gets `1` — not an incrementing counter. Anything that has to differ per
  occurrence belongs in the workflow that starts on the row.

⚠ **A member the target STOPS declaring is skipped, not fatal.** A schedule row outlives the deploys that reshape its
target, and failing the sweep over one stale field would stop a nightly job for everyone. It is reported in the
occurrence's note (see [audit read access (app.Audit)](https://osysharp.com/reference/config/audit/)) rather than silently ignored.

### The cadence is a set of rules, and each rule is a cross product   {#rules}
A schedule owns one or more **rules**. A rule has a frequency (`Once`, `Minute`, `Hour`, `Day`, `Week`, `Month`,
`Year`) and an interval (`1` = every, `2` = every other). Its **by-rule sets** — weekdays, days of the month, months,
times of day — combine as a **cross product**: two days × two times is **four** occurrences, not two.

A cross product cannot express *"Monday at 09:00 **and** Friday at 17:00"* — different times on different days. That
is two rules, and it is why a schedule owns a list of them rather than one.

### Nothing about when it fires comes from when you created it   {#required-sets}
Each frequency **requires** the sets that pin it:

| `Every` | required | why |
|---|---|---|
| `Minute`, `Hour` | — | a pure interval, measured from `EffectiveFrom` |
| `Day` | at least one time | otherwise its hour would come from nowhere |
| `Week` | weekdays + a time | otherwise its DAY would be whenever you happened to create the row |
| `Month` | days-of-month + a time | |
| `Year` | months + days-of-month + a time | |

`EffectiveFrom` is required for the same reason, and it does more than say when the schedule starts: it is the
**phase origin**. `Every = Week, Interval = 2` with `Day = Monday` says which weekday and nothing about which *week* —
the origin is what decides that, and the same is true of which quarter a 3-monthly rule lands in.

The result is that **two apps with the same schedule, created on different days, fire at the same instants.**

### Missed occurrences COALESCE     {#coalesce}
If nothing runs for three days, the schedule produces **one** row when it comes back, not seventy-two. The answer is
always the next occurrence after now; the ones that were missed are gone. This matches how repeating reminders behave
([Remind (milestone reminders)](https://osysharp.com/reference/workflow/remind/)).

They are gone, but they are **not unrecorded**: the occurrence that catches up says how many it absorbed, in the log
described under [History](#history).

### What if the last run has not finished?   {#overlap}
`Skip` (the default) passes over an occurrence while the previous one's work is still running, and the skipped
occurrence is **consumed, not deferred** — a job that overruns must not build a backlog it can never work off. `Allow`
says the work is safe to run concurrently with itself.

A skipped occurrence produces nothing, so it leaves no run and no row of your target entity. It leaves an occurrence
record — again, see [History](#history) — which is the only place a skip is visible.

### Does a daily 02:00 stay 02:00 across DST?   {#zones}
Every local time is interpreted in the schedule's `Zone`. A daily 02:00 is 02:00 *locally* across daylight-saving
boundaries, so consecutive occurrences are 23 or 25 hours apart in UTC twice a year rather than always 24.

⚠ The canonical maintenance hour is the one that **disappears**: in much of Europe and North America 02:00 does not
exist on the spring-forward date. The occurrence lands just after the gap, at 03:00 local — the same rule the platform
applies to every civil time it resolves.

### Exclusions, and how they differ from an end date   {#exclusions}
A `ScheduleExclusion` is a date range the schedule does not fire in — a holiday, a change freeze. It **suppresses
occurrences and does not end the schedule**: the cadence resumes after it. `EffectiveUntil` is the opposite; it ends
the schedule for good, and a lapsed schedule becomes `Retired` rather than being deleted, so *when did this last run*
survives the stopping.

⚠ **`EffectiveUntil` is an instant, not a day.** Setting it to a bare date means midnight that morning, which cuts off
that day's own occurrences — `2026-12-31` does not mean "through the 31st". Write `2026-12-31 23:59` if that is what
you meant.

### Naming the types   {#naming}
Every type may be written by its **full namespace**, as in C# — `new Osysharp.Scheduling.Schedule { … }`,
`Osysharp.Scheduling.ScheduleFrequency.Day` — or by its bare name once the app declares it. The examples below use the
qualified form because it reads unambiguously next to an app's own types; both are legal everywhere.

### Declaring who may reach a schedule   {#security}
The platform ships the SHAPE of a schedule and the app owns the ROWS, so — like the business-hours calendar
([ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/)) — nothing is readable or writable until the app says who may reach it:

```osy syntax
partial entity Schedule { security { allow read, create, update, delete when IsAuthenticated; } }
```

**One block covers the whole schedule.** The rules, their times, weekdays and month-days, and the exclusions are all
*part of* a schedule — they cannot exist without one — so they take that rule too ([rows that are part of another row](https://osysharp.com/reference/security/part-of-derived-access/)).
A schedule decomposing into six tables is a fact about the shape, not six decisions for you to make.

A child may still state its own rule, and it then stops deriving — one source for one answer, never a derived rule
sitting underneath an explicit one. Reach for that only when a child genuinely differs from its parent, which is
rarer than it sounds: a rule readable by someone who cannot read its schedule is usually a mistake rather than a
policy.

### Pausing   {#pausing}
Set `Status = Paused` to stop producing without losing the schedule or its phase; set it back to `Active` and the
platform arms the next occurrence from the rules. Neither needs any other change.

### Testing one — make 02:00 happen   {#testing}
A schedule is the one trigger with no person behind it, so a test cannot simply do the thing that starts it. Move the
clock past the occurrence and settle:

```osy syntax
TestClock.Advance(TimeSpan.FromDays(1));   // past the next occurrence
Background.Settle();                       // the sweep runs, the row is produced, its workflow starts

var run = DigestRun.Single(r => r.State == DigestStatus.Sent);
```

[`TestClock.Advance`](https://osysharp.com/reference/testing/clock-advance/) moves the clock and nothing else; the settle is what looks. One settle
covers the whole chain — the occurrence creates the target row, and anything that starts from that row (an
autostarting workflow, its entry body) runs in the same call.

⚠ **`Workflow.Settle(x)` is the wrong verb here and cannot be made to work.** It settles a named entity's run, and at
the moment a schedule fires there is nothing to name — the row is the occurrence's *output*. `Background.Settle()`
takes no argument for exactly that reason.

⚠ **One settle produces ONE occurrence, however far the clock jumped.** That is [[#coalesce|coalescing]], not a
limitation of the test surface: a week's advance on a nightly schedule yields a single row, which is what production
does after an outage. A test that expected seven would be encoding behaviour no deployment has.

### What ran, what was skipped, and why     {#history}
Each occurrence's run carries the ordinary workflow trail ([For(entity).Audit](https://osysharp.com/reference/workflow/audit/)). But a run only exists for an
occurrence that **produced** something, and the questions people actually ask a schedule are about the ones that did
not — so the platform also keeps an **occurrence log**: one row for every occurrence it considered, on the
`ScheduleOccurrenceRecord` audit surface ([audit read access (app.Audit)](https://osysharp.com/reference/config/audit/)).

Every consideration is recorded, **including the ordinary ones**, and that is the point rather than an excess. It is
what gives an *absent* row a meaning: no record for last night means the platform never looked, which is a different
fault, with a different fix, from a night that was skipped. A log holding only the exceptions leaves those two
indistinguishable — which is the state this exists to end.

Each row carries:

| field | what it says |
|---|---|
| `DueAt` | the instant the occurrence was **owed** |
| `At` | the instant the platform **looked** — on a catch-up these differ by the whole outage |
| `Outcome` | `Produced` · `Skipped` · `Retired` · `UnknownTarget` |
| `ProducedRow` | the row this occurrence created, on `Produced` |
| `CoalescedCount` | how many **further** occurrences this one absorbed; `0` on an ordinary night |
| `Note` | the sentence, when the outcome alone does not say it |

`UnknownTarget` means the schedule's target entity is no longer in the model — a deploy dropped the entity out from
under it. The schedule stays `Active` and keeps re-arming, so a later deploy that restores the entity resumes
production on its own; until then every occurrence is recorded as one of these.

It is an ordinary audit surface, so it is **read-only to your app**, closed until you open it, and bounded by the
window you declare. A schedule produces work whether or not anyone is watching, so unlike the other trails this one
is worth a `Retention`:

```osy title="occurrence-log" test app=scheduling-occurrence-log
[Principal] entity Operator { [Required] string Email; bool IsOnCall; }

entity DigestRun { }

app.Audit = new AuditConfig {
  ScheduleOccurrenceRecord = new AuditSurface {
    Read      = user => user.IsOnCall,
    Retention = TimeSpan.FromDays(90),
  }
};

// "What happened last night" — answered from data rather than from a log file. The surface names a TYPE as well as
// a set, so the rows come back and the caller sees WHICH occurrences and why, not just how many.
ScheduleOccurrenceRecord[] SkippedSinceYesterday() {
  return ScheduleOccurrenceRecord
    .Where(o => o.At > DateTime.UtcNow.AddDays(-1)
             && o.Outcome == Osysharp.Scheduling.ScheduleOccurrenceOutcome.Skipped)
    .ToList();
}
```

Read-only survives being held: a value of this type can be returned, passed and read, and a write to one is refused
wherever it is reached from. The rule is about the trail, not about the spelling that got you there.

## Examples       {#examples}

Nightly at 02:00 Stockholm time. The workflow autostarts on the row each occurrence creates:

```osy title="nightly" test app=scheduling-nightly
// `IsAuthenticated` asks whether there is a signed-in principal, so the app has to have one.
[Principal] entity Operator { [Required] string Email; }

// Baseline SHAPE, app-owned ROWS: the platform ships what a schedule IS and the app owns the rows, so under the
// security model the app must say who may reach them. Same requirement `ServiceHours` carries, and for the same
// reason — nothing is readable or writable until the app says so.
partial entity Schedule { security { allow read, create, update, delete when IsAuthenticated; } }

enum ReconcileStatus { Pending, Done }

entity ReconcileRun {
  ReconcileStatus State;
  DateTime? FinishedAt;
}

workflow Reconcile {
  Tracks    = ReconcileRun.State;
  Autostart = true;
  Initial   = Pending;

  state Pending {
    enter { this.Item.FinishedAt = DateTime.UtcNow; goto Done; }
  }
  terminal success Done { }
}

void SeedNightlyReconcile() {
  if (Osysharp.Scheduling.Schedule.Any()) { return; }        // idempotent — a second call mints no second schedule

  var nightly = new Osysharp.Scheduling.Schedule {
    Name          = "Nightly reconciliation",
    Template      = new ReconcileRun { },
    Zone          = "Europe/Stockholm",
    EffectiveFrom = DateTime.UtcNow
  };
  var rule = new Osysharp.Scheduling.ScheduleRule {
    Schedule = nightly,
    Every    = Osysharp.Scheduling.ScheduleFrequency.Day,
    Interval = 1
  };
  new Osysharp.Scheduling.ScheduleRuleTime { Rule = rule, At = TimeSpan.FromHours(2) };
}
```

Weekdays at 09:00 and 17:00 — six occurrences a week from one rule, because the sets cross-multiply:

```osy title="twice-daily-on-weekdays" test app=scheduling-weekdays
// `IsAuthenticated` asks whether there is a signed-in principal, so the app has to have one.
[Principal] entity Operator { [Required] string Email; }

// Baseline SHAPE, app-owned ROWS: the platform ships what a schedule IS and the app owns the rows, so under the
// security model the app must say who may reach them. Same requirement `ServiceHours` carries, and for the same
// reason — nothing is readable or writable until the app says so.
partial entity Schedule { security { allow read, create, update, delete when IsAuthenticated; } }

entity ShiftReport { }

void SeedShiftReports() {
  var s = new Osysharp.Scheduling.Schedule {
    Name          = "Shift report",
    Template      = new ShiftReport { },
    Zone          = "UTC",
    EffectiveFrom = DateTime.UtcNow
  };
  var rule = new Osysharp.Scheduling.ScheduleRule {
    Schedule = s,
    Every    = Osysharp.Scheduling.ScheduleFrequency.Week,
    Interval = 1
  };
  foreach (var d in [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday]) {
    new Osysharp.Scheduling.ScheduleRuleWeekday { Rule = rule, Day = d };
  }
  new Osysharp.Scheduling.ScheduleRuleTime { Rule = rule, At = TimeSpan.FromHours(9) };
  new Osysharp.Scheduling.ScheduleRuleTime { Rule = rule, At = TimeSpan.FromHours(17) };
}
```

Month-end billing, skipping a December freeze. `-1` is the last day of whatever month it lands in — the only way to
say "month end" across months of different lengths:

```osy title="month-end-with-a-freeze" test app=scheduling-monthend
// `IsAuthenticated` asks whether there is a signed-in principal, so the app has to have one.
[Principal] entity Operator { [Required] string Email; }

// Baseline SHAPE, app-owned ROWS: the platform ships what a schedule IS and the app owns the rows, so under the
// security model the app must say who may reach them. Same requirement `ServiceHours` carries, and for the same
// reason — nothing is readable or writable until the app says so.
partial entity Schedule { security { allow read, create, update, delete when IsAuthenticated; } }

entity Account { [Required, MaxLength(60)] string Name; }
entity BillingRun { Account Account; [MaxLength(20)] string Kind; }

void SeedBilling() {
  var acct = new Account { Name = "Acme" };

  var s = new Osysharp.Scheduling.Schedule {
    Name          = "Month-end billing",
    // The template carries CONTEXT: every occurrence mints a BillingRun pointing at this account.
    Template      = new BillingRun { Account = acct, Kind = "month-end" },
    Zone          = "UTC",
    EffectiveFrom = DateTime.UtcNow
  };
  var rule = new Osysharp.Scheduling.ScheduleRule {
    Schedule = s,
    Every    = Osysharp.Scheduling.ScheduleFrequency.Month,
    Interval = 1
  };
  new Osysharp.Scheduling.ScheduleRuleMonthDay { Rule = rule, Day = -1 };
  new Osysharp.Scheduling.ScheduleRuleTime { Rule = rule, At = TimeSpan.FromHours(23) };

  new Osysharp.Scheduling.ScheduleExclusion {
    Schedule = s,
    From     = DateTime.Parse("2026-12-20"),
    To       = DateTime.Parse("2027-01-02"),
    Reason   = "Change freeze"
  };
}
```

## See also       {#see-also}
- [Tracks and Initial (the field a workflow drives)](https://osysharp.com/reference/workflow/tracks/) — the entity a workflow tracks, which is what a schedule's `Template` names
- [Remind (milestone reminders)](https://osysharp.com/reference/workflow/remind/) — reminders inside a run, and the same coalescing rule for missed cadences
- [ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/) — the business-hours calendar an SLA clock accrues against (a different job from a schedule)
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — the trail each occurrence's run leaves
- [audit read access (app.Audit)](https://osysharp.com/reference/config/audit/) — `app.Audit`, which gates and bounds the `ScheduleOccurrenceRecord` occurrence log


---

<!-- https://osysharp.com/reference/security/oauth-completion/ -->

# OAuth account completion — Security.VerifyPendingOAuthEmail / LinkOAuthFromPending

> The two server-side calls that finish an OAuth sign-in on your own pages. When someone signs in with a provider (Google, …) and has no account yet — or a local account that isn't linked — the callback hands your app a sealed token. `VerifyPendingOAuthEmail` reads the provider-VERIFIED email out of that token (the email is inside the seal, never a value the browser can set), and `LinkOAuthFromPending` records the identity link. You provision through your own `[AuthMethod]`, so the account model stays yours.

<!-- id: security-oauth-completion · area: security · stability: stable · html: https://osysharp.com/reference/security/oauth-completion/ -->

## Summary        {#summary}
When a visitor signs in with an OAuth provider and there is **no account for them yet**, the provider sign-in cannot be
the whole story — you still need their name, or a decision to link. So the platform validates the provider identity on
the server, **seals the verified email into a short-lived token**, and sends the visitor to a page of *yours* with that
token. Your page collects what it needs and calls an `[AuthMethod]` that finishes the job — creating a proper,
no-password account and recording the identity link:

```osy title="finish a new OAuth sign-in — create the account, link the identity, issue the ticket" test app=security-oauth-completion
[AuthMethod]
string CompleteOAuthSignup(string pendingToken, string firstName, string lastName) {
  var email = Security.VerifyPendingOAuthEmail(pendingToken);   // the VERIFIED email, read from inside the sealed token
  if (email == "") { return ""; }                               // invalid / expired / unverified → no ticket
  var u = new User { Email = email, FirstName = firstName, LastName = lastName };
  Security.LinkOAuthFromPending(u.Id, pendingToken);            // record the identity link (the provider subject stays sealed)
  return Security.IssueJwt(u.Id, u.Email);                      // signed up == signed in
}
```

The email is a **return value, not a parameter** — that is the whole point. A browser could type any email into a form,
but it cannot forge the sealed token, so `VerifyPendingOAuthEmail` hands back only an address the provider actually
verified. Your function trusts it because it came out of the seal, not off the wire.

## Signature      {#signature}
```osy syntax
string Security.VerifyPendingOAuthEmail(string token)   // the provider-verified email inside the token, or "" if it is
                                                        // invalid, expired, for another app, or not provider-verified
bool   Security.LinkOAuthFromPending(Guid userId, string token)   // record the identity link for this user; false on a bad token
```

Both are **server-only** (like [`Security.IssueJwt`](https://osysharp.com/reference/stdlib/security/)): the token is sealed with your app's
platform-managed key, so only the server can open it. They run inside an `[AuthMethod]` — a function a signed-out
visitor may call — wired into [`app.AuthBootstrap`](https://osysharp.com/reference/security/auth-bootstrap/) as `OAuthSignup` / `OAuthLink`.

## Description    {#description}

### What happens when a provider sign-in comes back?   {#outcomes}
A provider sign-in resolves to one of three things, and the platform decides which by looking at the verified email:

| Situation | What happens |
|---|---|
| The identity is **already linked** | Signed straight in — your pages never see it. |
| **No account** has this email | The visitor lands on your signup-completion page with a token; you call `CompleteOAuthSignup`. |
| A **local account** has this email but no link | The visitor lands on your link-confirm page; you call an `[AuthMethod]` that verifies the token and calls `LinkOAuthFromPending` for the account already found by that email. |

The second and third are *yours* to render and provision — the platform only carries the verified identity to you,
sealed, and back.

### Linking an existing account   {#linking}
When the email already belongs to a local (say, password) account, you don't create anything — you attach the provider
identity to the account already there, so next time "Continue with Google" signs them straight in:

```osy title="link a provider identity to the existing account with that email" test app=security-oauth-completion
[AuthMethod]
string LinkOAuthAccount(string pendingToken) {
  var email = Security.VerifyPendingOAuthEmail(pendingToken);
  if (email == "") { return ""; }
  var u = User.Where(x => x.Email == email).FirstOrDefault();   // the account is found by the SEALED email, not client input
  if (u == null) { return ""; }
  Security.LinkOAuthFromPending(u.Id, pendingToken);
  return Security.IssueJwt(u.Id, u.Email);
}
```

### Why the email is safe to trust   {#trust}
Everything an `[AuthMethod]` receives from a page is untrusted — including the token. What makes this safe is that the
token is **sealed with your app's key and bound to your app**: a browser cannot mint one, cannot alter the email inside
it, and cannot replay one minted for a different app. So the email that comes *out* of `VerifyPendingOAuthEmail` is
exactly the one the provider verified, even though it arrived through the visitor's browser. If you passed the email in
as a plain argument instead, anyone could complete a signup for anyone else's address — which is the mistake this
surface exists to make impossible. The provider *subject* (the stable id the link is keyed on) never leaves the seal at
all; `LinkOAuthFromPending` writes it for you.

### How are the completion functions wired?   {#wiring}
The completion functions are ordinary `[AuthMethod]`s, wired into `app.AuthBootstrap` so a signed-out visitor may call
them, and armed with the same ephemeral role as `Login`/`Signup` — so what they may write is exactly your
[`security { }`](https://osysharp.com/reference/security/entity-security/) grants for that role, nothing more:

```osy title="the principal, and the auth entry points wired together" test app=security-oauth-completion
[Role] enum AppRole { Authenticator, Member }

[Principal]
entity User {
  [Required, Unique, MaxLength(255)] string Email;
  [MaxLength(200)] string FirstName;              // collected on the signup-completion form
  [MaxLength(200)] string LastName;
  [MaxLength(200)] string? PasswordHash;          // empty for an OAuth-only account — it has no password
  security {
    allow read, create when IsAuthenticator;      // the auth flow finds/creates the account
    allow read where Id == user.Id;               // and a signed-in user reads their own row
    deny read PasswordHash when !IsAuthenticator;
  }
}

entity RoleGrant {
  [Required] User User;
  [Required] AppRole Role = AppRole.Member;
  security { allow create when IsAuthenticator; }
}

policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);

[AuthMethod]
string Login(string email, string password) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  // Spend the same time either way — an early return makes "no such account" measurably faster
  // than "wrong password", and that difference is an enumeration oracle anyone can time. The
  // one-argument form verifies against nothing, costs a full KDF, and answers false.
  if (u == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); }
  return "";
}

app.AuthBootstrap = new AuthBootstrap {
  Role        = AppRole.Authenticator,
  Login       = Login,
  OAuthSignup = CompleteOAuthSignup,     // the new-account page calls this
  OAuthLink   = LinkOAuthAccount,        // the link-confirm page calls this
};
```

Your completion page reads the token out of the URL fragment it was sent to — see [`Navigation.Hash`](https://osysharp.com/reference/ui/navigation/)
(`Text.Split(Navigation.Hash, "pending_oauth=")`) — and passes it to the `[AuthMethod]`; the returned ticket goes to
`Session.SignIn`, exactly like a password login.

## Examples       {#examples}
The three fences above assemble into one working app: a `[Principal]` with an OAuth-ready model, a password `Login`, and
the two OAuth `[AuthMethod]`s wired into `app.AuthBootstrap`. Compile it and both "Continue with Google" outcomes —
brand-new account and link-an-existing-one — are handled on your own pages, in your own account model.

### Testing it — `TestOAuth.PendingToken`   {#testing}
The token these verbs read is minted by the platform and SEALED with the host's own key, which is exactly what makes
trusting the email inside it safe — and exactly what makes it impossible to write one by hand in a test. So there is
a verb that mints a real one:

```osy title="a sealed pending token, so the completion path can be tested at all" syntax
[Test] void a_verified_email_completes_the_account() {
  var token = TestOAuth.PendingToken("ada@example.com");
  Assert.Equal("ada@example.com", Security.VerifyPendingOAuthEmail(token));
}
```

⚠ **TEST-ONLY** — it is refused outside a `[Test]` / `[TestFixture]` body, and it runs on the server for the same
reason the seal exists: the key is the host's, so nothing else can produce one. Each call mints a fresh token, so
two calls in one test are two different tokens.

## See also       {#see-also}
- [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/) — `[AuthMethod]`, the marker that lets a signed-out visitor call these
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — `app.AuthBootstrap`, where `OAuthSignup`/`OAuthLink` are wired
- [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/) — `IssueJwt`, `VerifyPassword`, and the rest of the auth toolkit
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — granting a role once an account exists


---

<!-- https://osysharp.com/reference/security/index/ -->

# The security model

> How authorization works in Osy#, end to end. Everything is denied until you grant it; a grant is compiled into every query rather than checked afterwards; and the only way to know a rule works is to become the user and try.

<!-- id: security-index · area: security · stability: stable · html: https://osysharp.com/reference/security/index/ -->

## Summary        {#summary}
Security in Osy# rests on three ideas. Hold these and the rest is detail:

1. **Silence denies.** An entity you say nothing about is readable by nobody. You never turn access off — you only
   ever grant it.
2. **A grant is part of the query.** A row you may not see is not fetched and then hidden; it is never selected. So
   `Order.Count()` honestly means *"how many orders exist **for me**"*, and two users can correctly get different
   numbers from the same function.
3. **A rule you have not tested is a rule you only believe you wrote.** Security is the one area where compiling,
   passing tests and "working" tell you nothing about correctness. The only proof is to become the other user and be
   refused.

The rest of this page is the model those three ideas describe.

## Description    {#description}

### Which page answers which question?   {#shape}

| Question | Answered by | Page |
|---|---|---|
| What may this entity's rows be used for, and by whom? | the `security { }` block | [security { }](https://osysharp.com/reference/security/entity-security/) |
| *Is this row yours?* | a `where` clause — a predicate over the **row** | [security { }](https://osysharp.com/reference/security/entity-security/) |
| *Is it yours through something else?* | a `where` clause that **navigates relations**, any depth | [navigation in security predicates (any depth, either side)](https://osysharp.com/reference/security/navigation-predicates/) |
| *Are you the kind of person who may do this at all?* | a `when` clause — a predicate over the **principal** | [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) |
| Who is `user`? | the `[Principal]` entity | below |
| May this person open this page? | `[Authorize(policy)]` on the component | [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) |
| How does anyone log in, if nothing is readable yet? | `app.AuthBootstrap` | [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) |
| Does any of it actually work? | `runas` + `Assert.Denied` | [runas](https://osysharp.com/reference/testing/runas/) |

### 1. Silence denies   {#silence-denies}
The posture is deny-all ([secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/)). An entity with no `security { }` block is denied to every
user request — not "readable by signed-in users", not "readable by its owner". **Denied.**

To lock an entity completely, say nothing:

```osy title="locked, by saying nothing" test app=security-index
entity AuditRecord {
  [MaxLength(200)] string Message;
  // No security block. No user can read it, and no user can create one.
}
```

There is therefore no `default deny` to write, and a `security { }` block is a **list of grants**. This is why the
failure mode of forgetting a rule is *"nobody can do it"* — reported within the minute — rather than *"everybody
can"*, which nobody reports at all. A door that fails shut is a door you can trust.

### 2. Who `user` is   {#principal}
One entity in your app is the **principal** — the thing a logged-in person *is*. Mark it `[Principal]`, and `user`
inside a security rule means a row of it:

```osy title="the principal, and a rule that compares against it" test app=security-index
[Principal] entity User {
  [Required] string Name;
}

entity Doc {
  User Owner;
  [MaxLength(200)] string Title;
  security { allow read where Owner == user; }   // `user` IS the acting principal's row
}
```

The principal is **yours** — your entity, your fields, your extra relationships. The platform does not impose a user
model on you; it only needs to know which of your entities is the one people log in as.

**Roles are yours too, and they are just data.** A role is granted by an ordinary entity: anything that references the
`[Principal]` and carries a member of your `[Role]` enum *is* a grant table, recognised by that shape. So "who may be
an admin" is not a platform setting — it is the question "who may create a row in that table", answered by a
`security { }` block like any other. One rule is worth carrying from the start: **an app has exactly ONE `[Role]`
enum** — the vocabulary the login ticket carries. Every other tier (org membership, project membership, a
team's `Owner`/`Member`) is **ordinary data**, and you query it. Both kinds work in a rule; they are simply answered
differently, and [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) lays them out side by side — along with why a *second* `[Role]` enum is a
compile error rather than a convenience.

### 3. Two axes: the row, and the person   {#two-axes}
This is the distinction to internalise, because almost every real rule is one or the other:

- **`where`** filters by the **row**. *The owner sees their own documents.* It narrows **which rows** you get.
- **`when`** gates by the **principal**. *Staff see every document.* It decides **whether you may at all**.

Name the principal test with a `policy` so it is written once and reused everywhere:

```osy title="both axes, and a named policy" test app=security-index
[Role] enum AppRole { Staff, Admin }     // the app's ONE role vocabulary

entity RoleGrant {                        // a [Principal] ref + a [Role] member ⇒ this table grants roles
  User Grantee;
  [Required] AppRole Level;
}

policy IsStaff => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Staff);

entity Report {
  [MaxLength(200)] string Title;
  security {
    allow read when IsStaff;              // by PERSON: staff, and nobody else
  }
}

entity Memo {
  User Owner;
  [MaxLength(200)] string Note;
  security {
    allow read when IsStaff;              // staff read every memo …
    allow read where Owner == user;       // … and everyone reads their own
    allow update where Owner == user;     // but only the author may change one
  }
}
```

Grants **add up**: a staff member who also owns a memo is covered by either rule. And note that read and write are
separate grants — "the team can see it, only the author can change it" is the common case, not an exotic one.

### 4. The grant is inside the query   {#in-the-query}
A rule is compiled into the SQL alongside your own predicate. It is not a filter applied to rows you already
fetched, and it is not a check you are expected to remember to call.

Three consequences worth stating plainly:

- **`Count()` is honest.** Under `allow read where Owner == user`, `Doc.Count()` returns *your* documents' count. Two
  users get different numbers and both are right.
- **You never re-check after a query.** There is no "and now verify they were allowed to see these". If a row came
  back, they were allowed.
- **You cannot leak by forgetting.** There is no code path — a function, an API call, an MCP tool, a UI query — that
  goes around it, because there is no "it" to go around. The rule is the query.

### 5. The bootstrap paradox   {#bootstrap}
If nothing is readable until you are authenticated, how does anyone *log in* — an act that must read the user row of
someone who, by definition, is not yet authenticated?

`app.AuthBootstrap` resolves it: it names a role the engine mints a **user-less ephemeral principal** with, and the
functions (login, signup, password reset) it runs under that principal. Crucially, what that principal may touch is
your ordinary `security { }` grants — **no special access is minted**, so the bootstrap path cannot become a hole you
forgot about. See [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/).

### 6. The UI has its own rail   {#ui}
Data security and page security are different questions, and both are deny-first:

- A routed component **requires authentication** unless it says otherwise ([page authorization (policies)](https://osysharp.com/reference/ui/authorize/)).
- `[Authorize(policy)]` requires a named policy — and the reference is **compile-checked**, so `[Authorize(Typo)]` is
  a build error rather than a silent hole.

But understand the layering: **the UI rail decides who may open a page; the entity rules decide what data exists for
them.** A page that forgets `[Authorize]` and a query that correctly filters by owner will still show nothing but the
user's own rows. Defence in depth is not a slogan here; the data path never trusts the UI path.

### 7. Prove it, or you have not done it   {#prove-it}
Compiling proves nothing. Your tests passing proves nothing — they probably ran unrestricted. The only way to know
that Bob cannot read Alice's document is to **become Bob and be refused**:

```osy title="the test that actually proves the rule" run app=security-index
// `principal` names a seeded row so the test can BE that person. It resolves unsecured — a `[Test]` body outside
// a `runas` is an anonymous caller, so a `User.Single(…)` written there would read nothing.
principal Bob => User.Single(u => u.Name == "Bob");

[TestFixture]
void Seed() {
  var alice = new User { Name = "Alice" };
  var bob = new User { Name = "Bob" };
  var aliceDoc = new Doc { Owner = alice, Title = "alice-doc" };
}

[Test(Seed)]
[runas(Bob)]
void Bob_cannot_see_Alices_doc() {
  Assert.Equal(0, Doc.Count());     // for Bob, Alice's row does not exist — it is not hidden, it is absent
}
```

Note what is being asserted: not that an exception was thrown, but that **the row is not there**. That is the shape of
a correct row-level rule.

Write one of these for every rule that matters. It is what stops a refactor a year from now from quietly opening a
door that nobody notices is open — and "nobody notices" is the entire failure mode of security.

### The mistakes people make   {#mistakes}

| Mistake | What actually happens |
|---|---|
| Writing `default deny;` | Nothing. It is already the default — and typing it teaches you that security exists *where you typed it*. |
| A bare `allow read;` | A **compile error** on an app with a principal. Say *who*: `when IsAuthenticated`, or `IsAuthenticated \|\| IsAnonymous` if you truly mean the whole internet. |
| Fetching rows and filtering them in a `foreach` | Slower, and it is not security — the rows already left the database. Put the predicate in the query. |
| Testing with no `runas` | You tested what an **unrestricted** caller can do, which is everything. You have not tested security. |
| Assuming the UI protects the data | It does not, and it is not supposed to. The data path never trusts the UI path. |

## See also       {#see-also}
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — the deny-all posture in full, and what it does not cover
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block: `allow`, `where`, `when`, `policy`
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — `IsAuthenticated` / `IsAnonymous`, and why an open read must say so
- [navigation in security predicates (any depth, either side)](https://osysharp.com/reference/security/navigation-predicates/) — following relations in a `where`, at any depth and on either side of the comparison
- [public pages (what a signed-out visitor can see and do)](https://osysharp.com/reference/security/public-reads/) — a public page and its public data are two grants; forget the second and the page renders empty
- [capability rows that belong to a user](https://osysharp.com/reference/security/capability-row-ownership/) — capability tables whose rows belong to one signed-in user, and the `[Principal]` they need
- [rows that are part of another row](https://osysharp.com/reference/security/part-of-derived-access/) — rows that are part of another row and take its rule; declare a shape's security once
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — logging in before a principal exists
- [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/) — `[AuthMethod]`: the one door an unauthenticated visitor may walk through
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — granting a role, the first admin, and never letting a lower tier grant a higher one
- [app.Auth — how the platform authenticates a user of your app](https://osysharp.com/reference/security/password-auth/) — `app.Auth`: how the platform can authenticate a user of your app with no code from you
- [OAuth clients (app.OAuthClients)](https://osysharp.com/reference/config/oauth-clients/) — `app.OAuthClients`: signing users in with an external identity (Google/GitHub/…), and connecting to an external API on their behalf
- [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/) — `HashPassword` · `VerifyPassword` · `IssueJwt` · `RandomId`
- [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) — `[Authorize(policy)]` on a page
- [runas](https://osysharp.com/reference/testing/runas/) — becoming a user, so a rule can be proved


---

<!-- https://osysharp.com/reference/security/auth-method/ -->

# [AuthMethod] — a function an unauthenticated visitor may call

> `[AuthMethod]` marks a sign-in function — login, signup, password-reset — as reachable by a visitor who is not signed in. Everything else in your app refuses an unauthenticated caller before it runs, which is what you want; sign-in is the one flow that cannot require the thing it produces. The marker is checked against `app.AuthBootstrap` **both ways**: a marked function must be wired, and a wired function must be marked. So an anonymous entry point cannot exist by accident.

<!-- id: security-auth-method · area: security · stability: stable · html: https://osysharp.com/reference/security/auth-method/ -->

## Summary        {#summary}
A function marked **`[AuthMethod]`** may be called by a visitor who is **not signed in**. Every other function in your
app refuses an unauthenticated caller before its first statement runs — which is exactly what you want, and is why
sign-in needs a marker of its own: `Login` cannot require a signed-in user, because producing one is its job.

```osy title="the login function a signed-out visitor may call" test app=security-auth-method
[AuthMethod]
string Login(string email, string password) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  // Spend the same time either way — an early return makes "no such account" measurably faster
  // than "wrong password", and that difference is an enumeration oracle anyone can time. The
  // one-argument form verifies against nothing, costs a full KDF, and answers false.
  if (u == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); }
  return "";           // no match → no ticket → the caller stays anonymous
}
```

The marker alone is not enough, and that is deliberate: the function must **also** be wired into
[`app.AuthBootstrap`](https://osysharp.com/reference/security/auth-bootstrap/) (as `Login`, `Signup` or `PasswordReset`). The two halves are checked
against each other, so you cannot get one without the other.

## Signature      {#signature}
```osy syntax
[AuthMethod] <ReturnType> <Name>(<params>) { … }   // callable while signed out; must be wired in app.AuthBootstrap
```

A `Login` or `Signup` returns the **session ticket** — a `string` from [`Security.IssueJwt`](https://osysharp.com/reference/stdlib/security/) — or
`""` for "no". A `PasswordReset` returns nothing of value **on purpose**: it mints a token, records it, and mails it —
and handing that token back to the caller instead would let anyone who knows an address take the account. Returning
nothing is the security property, not a shrug.

⚠ **All three verbs, or the flow cannot complete.** Mint and record and *send*. A reset that mints a token and stops
leaves the confirm step asking for a value nobody can obtain — and it will not look broken: it compiles, it
validates, and its tests can pass, because a test may read the token out of the row as the auth principal and the
locked-out person cannot. `osy lint` reports it as `security-reset-token-never-delivered` (MUST). The platform has no
mail verb; delivery is a [`client { }`](https://osysharp.com/reference/http/client/) you declare. See [[security-auth-bootstrap#examples]].

## Description    {#description}

### Do I need both `[AuthMethod]` and `app.AuthBootstrap`?   {#both-ways}
`[AuthMethod]` and `app.AuthBootstrap` must agree, and the compiler enforces it in **both directions**:

| You wrote | What happens | Why |
|---|---|---|
| `[AuthMethod]` on a function **not** wired in `app.AuthBootstrap` | **compile error** | It is an anonymously-reachable entry point that no sign-in flow uses. That is a hole, and a hole is never intentional. |
| A function wired in `app.AuthBootstrap` **without** `[AuthMethod]` | **compile error** | The signed-out visitor would be refused before your login could run — a login page that can never log anyone in. |
| Both | it works | The only way to get an anonymous entry point is to say so twice. |

There is no `[AllowAnonymous]` on a function. That marker belongs to pages ([page authorization (policies)](https://osysharp.com/reference/ui/authorize/)); a function opens to the
world only through this one purpose-built door, so `grep AuthMethod` is a complete list of your app's anonymous entry
points. It should be a short list.

### What an auth method may touch   {#the-leash}
An `[AuthMethod]` does **not** run with special powers. It runs as the **ephemeral principal** described in
[auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — a caller bearing the one `[Role]` you named, and **no user**. What it may read and write
is decided by your ordinary [`security { }`](https://osysharp.com/reference/security/entity-security/) grants for that role, and nothing else:

```osy title="the leash — the auth role is granted exactly what login needs, and no more" test app=security-auth-method
[Role] enum AppRole { Authenticator, Member, Admin }

[Principal]
entity User {
  [Required, Unique, MaxLength(255)] string Email;
  [MaxLength(200)] string PasswordHash;
  security {
    allow read   when IsAuthenticator;             // login must find the user and check the hash
    allow create when IsAuthenticator;             // signup must create one
    allow read where Id == user.Id;                // and a signed-in user reads their own row
    deny read PasswordHash when !IsAuthenticator;  // nobody else EVER reads the hash — not even an admin
  }
}

entity RoleGrant {
  [Required] User User;
  [Required] AppRole Role = AppRole.Member;
  security { allow create when IsAuthenticator; }  // the auth flow may mint a grant…
}

entity Invoice {                                    // …and it may not touch anything else.
  [Required] decimal Total;
  security { allow read where User.Any(u => u.Id == user.Id); }
}

policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);
```

`Login` above can read `User` — including `PasswordHash`, which the field mask hands to no one else. It cannot read
an `Invoice`, because nothing granted `Authenticator` a read on one. If you never grant the auth role a write, no
routed method can be turned into a writer, however the function is written. **The leash is your own rules**, which is
the whole point: the bootstrap path is not a hole you have to remember — it is subject to the same grants you can read
on the entity.

### It cannot elevate a caller who is already signed in   {#no-elevation}
The ephemeral principal is minted **only** when the caller has no authenticated ticket. An already-signed-in user who
calls `Login` is *not* elevated — they run as themselves, with their own grants. So the bootstrap path can never be
used by an ordinary user to borrow the auth role's access.

### What should a login RETURN, and what does a failure return?   {#ticket}
A `Login`/`Signup` ends by returning what [`Security.IssueJwt(userId, email)`](https://osysharp.com/reference/stdlib/security/) produced. That string
is the session ticket, scoped to your app and that user. The login page hands it to `Session.SignIn(ticket)`, which
stores it as the session bearer, so every later request is authenticated as that user.

**A failed sign-in returns `""`** — not an exception, not a partial ticket. `Session.SignIn("")` stores nothing and the
visitor stays anonymous. Returning the same empty answer for "no such user" and "wrong password" is also what keeps
`Login` from telling a stranger which email addresses have accounts.

## Examples       {#examples}
Signup, wired as the second auth method — it creates the credential and signs the new user straight in:

```osy title="signup: create the credential, issue the ticket" test app=security-auth-method
[AuthMethod]
string Signup(string email, string password) {
  var u = new User { Email = email, PasswordHash = Security.HashPassword(password) };
  var grant = new RoleGrant { User = u, Role = AppRole.Member };
  return Security.IssueJwt(u.Id, u.Email);
}

[Page("/login")]
[AllowAnonymous]
[Render(CSR)]
component LoginPage() {
  string email = "";
  string password = "";
  action SignIn() { Session.SignIn(Login(email, password)); }
  render {
    Input(value: email, placeholder: "Email");
    Input(value: password, placeholder: "Password", type: "password");
    Button("Sign in", onPress: SignIn);
  }
}

app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };
app.AuthBootstrap = new AuthBootstrap {
  Role      = AppRole.Authenticator,
  Login     = Login,
  Signup    = Signup,
  LoginPage = LoginPage,
};
```

Both `Login` and `Signup` are marked **and** wired — the pairing the compiler insists on. `Signup` hashes the password
(never storing the plaintext), grants the ordinary `Member` role, and returns a ticket, so signing up *is* signing in.
For the variant where the **first** account bootstraps an admin, see [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/).

## See also       {#see-also}
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — the wiring an `[AuthMethod]` must appear in, and the ephemeral principal it runs as
- [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/) — `HashPassword` · `VerifyPassword` · `IssueJwt`
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — granting a role during signup without opening a path to self-elevation
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` grants that leash the auth role
- [app.Auth — how the platform authenticates a user of your app](https://osysharp.com/reference/security/password-auth/) — `app.Auth`, the *other* way a user of your app can be authenticated


---

<!-- https://osysharp.com/reference/security/acting-for-another-principal/ -->

# acting for another principal

> A shared terminal, a kiosk, a scanner or a back-office integration is a **user of your app**, not a mode of it. It holds its own `[Principal]` row and its own `[Role]`, it authenticates as ITSELF (an API key or a password), and the person it is acting for is an ORDINARY PARAMETER of the call — checked by your own `security {}` and by your own function, never taken on trust. Nothing outside a `[Test]` mints a credential for somebody who did not present one, so there is no "become this user" verb to look for. The role and the key are minted from the CLI — `osy user add <login> --role <R>` writes the principal row AND its role grant, `osy user apikey generate <login>` prints the key once — which is how an app with no admin UI gets its first privileged account.

<!-- id: security-acting-for-another-principal · area: security · stability: stable · html: https://osysharp.com/reference/security/acting-for-another-principal/ -->

## Summary        {#summary}
A shared device — a hallway terminal, a warehouse scanner, a front-desk tablet — and a back-office integration are
the same problem wearing two costumes: **something that is not a person files work that belongs to a person.**

Osy# answers it with pieces you already have, and with no new concept:

1. the device **is a user**. It holds its own `[Principal]` row and its own `[Role]`, exactly like a person.
2. it **authenticates as itself** — its own API key or its own password. It never presents anybody else's.
3. the person it acts FOR is an **ordinary parameter** of the call. `user` stays the device.
4. **you** decide, in the function and in `security {}`, which subjects that role may name.
5. the role and the key are minted **from the CLI**, so an app with no admin UI still has a way to get its first
   privileged account.

⛔ **There is no "act as this user" verb, and looking for one is the wrong turn.** `runas`, `Ui.SignInAs` and
`Api.KeyFor` all exist and are all **refused at compile time outside a `[Test]`** — minting a working credential for
someone who did not present one is impersonation anywhere else. `osy run --as` and `osy import --as` are real, and
they are not an exception: they take **that principal's own password**. The absence is deliberate, and it is why the
design above is the design rather than a workaround for a missing feature.

## Signature      {#signature}
```osy syntax
// 1 — the device is a principal with a role of its own
[Role] enum StaffRole { Authenticator, Courier, Scanner }

// 2 — the role is a policy over your own grant table, like any other
policy IsScanner => RoleGrant.Any(g => g.Holder == user && g.Level == StaffRole.Scanner);

// 3 — the SUBJECT is a field; the rule admits the subject themselves OR the device
entity Handover {
  [Required("…")] Account Courier;    // whose work this is  — a PARAMETER at the call site
  [Required("…")] Account FiledBy;    // who pressed the button — never a parameter
  security {
    allow create where Courier == user;   // the courier files their own
    allow create when IsScanner;          // …and the shared scanner files anyone's
  }
}
```
```bash
# 4 — mint the account AND its role in one act; then its key, printed once
osy user add scanner@depot.example --role Scanner --password '…'
osy user apikey generate scanner@depot.example
```

## Description    {#description}

### Why the obvious design does not work   {#why-not-one-key}
The first design everybody writes is *"give the device an API key and let it post whoever's name is on the screen"*.
It does not work, and the reason is a single sentence from [[api-rest#who-is-user]]:

> **An API key is a PER-USER credential. There is no app-wide API key.** A key is minted against exactly one
> `[Principal]` row, and a request carrying it runs **as that user**.

So a request from the terminal has `user` = the terminal. Every rule you wrote in the shape
`allow read where Owner == user` therefore matches the terminal's own rows — that is, none — and the natural next
move is the wrong one: opening the entity to `IsAnonymous`, or dropping `Auth` from the API so the route "works".
That opens the same entity on every other surface too, to the whole internet.

⚑ **The fix is not a second kind of credential. It is admitting the device is a second kind of USER.** Once the
device has its own row and its own role, `user` being the device is exactly right — and everything below is ordinary
Osy#.

### The device holds its own account, its own role   {#the-integration-is-a-principal}
Nothing about the device's account is special. It is a row of the same `[Principal]` entity a person gets, with a
grant of a role you invented for it:

```osy title="the app: three roles, one of them a machine's" test app=security-acting-for-another-principal
[Role] enum StaffRole { Authenticator, Courier, Scanner }

[Principal] entity Account {
  [Unique, MaxLength(200)] string Email;
  [MaxLength(200)] string PasswordHash;
  [MaxLength(255)] string? ApiKeyHash;    // the key in force — see [[api-rest#api-key-storage]]
  [MaxLength(255)] string? ApiKeyHash2;   // the rotation slot
  security {
    allow read when IsAuthenticated;
    allow read, create when IsAuthenticator;
    // ⛔ ALL THREE. A credential with no field-level `deny read` rides `Session.CurrentUser` to the browser.
    deny read PasswordHash when !IsAuthenticator;
    deny read ApiKeyHash   when !IsAuthenticator;
    deny read ApiKeyHash2  when !IsAuthenticator;
  }
}

entity RoleGrant {
  [Required("Name the account this grant belongs to.")] Account Holder;
  StaffRole Level;
  security {
    allow read when IsAuthenticated;
    allow create when IsAuthenticator;
  }
}

policy IsAuthenticator => RoleGrant.Any(g => g.Holder == user && g.Level == StaffRole.Authenticator);
policy IsScanner       => RoleGrant.Any(g => g.Holder == user && g.Level == StaffRole.Scanner);
```

`Scanner` is a role like any other. It is not privileged by being a machine's — it is privileged by exactly the
grants you write for it, and by nothing else.

### Two grants, and the second is the whole feature   {#the-rule-shape}
The entity that records the work carries the subject as a **reference**, and its `security {}` admits two callers:
the subject acting for themselves, and the device acting for them.

```osy title="the record: whose work it is, and who filed it" test app=security-acting-for-another-principal
entity Handover {
  [Required("Say which courier took the parcel.")] Account Courier;
  [Required("Every handover records who filed it.")] Account FiledBy;
  [Required("A parcel reference is what makes the handover findable.")] [MaxLength(40)] string Parcel;
  security {
    allow read   where Courier == user;   // a courier reads their own handovers
    allow create where Courier == user;   // …and files their own
    allow create when IsScanner;          // …and the shared scanner files anyone's
  }
}
```

The two `allow create` lines are read as an OR, and the difference between them is the whole mechanism:

| the line | what it is | who it admits |
|---|---|---|
| `allow create where Courier == user` | a **row filter** — it correlates a column to the caller | the subject, for their own rows only |
| `allow create when IsScanner` | a **guard** — a predicate about the caller, with no column in it | the device, for anybody's rows |

⚠ **A `where` rule can never express "on behalf of", and reaching for one is the commonest wrong turn.** A `where`
correlates the ROW to the CALLER, and the whole point here is that the row belongs to somebody who is not the caller.
The device's grant is therefore a `when`, and everything that narrows it lives in the function — the next section.

### What stops the device naming anyone it likes   {#stopping-a-forged-subject}
`allow create when IsScanner` is a broad grant on purpose: it says the scanner may file for **somebody else**, and no
declarative rule can say which somebody, because the answer is your app's own business logic. Three things narrow it,
and all three are ordinary code:

```osy title="the endpoint: the subject is an argument, the actor is not" test app=security-acting-for-another-principal
void RecordHandover(string courierEmail, string parcel) {
  var courier = Account.Where(a => a.Email == courierEmail).FirstOrDefault();
  if (courier == null) { throw new NotFoundException("No account with that address."); }

  // ⛔ A SUBJECT THE CALLER CAN NAME IS NOT A SUBJECT THE CALLER MAY USE. Without this line the scanner in the
  //    loading bay can file a handover against the finance director, who is also an Account. The predicate is
  //    yours to choose; having one is not optional.
  if (!RoleGrant.Any(g => g.Holder == courier && g.Level == StaffRole.Courier)) {
    throw new ValidationException("That account is not a courier.");
  }

  // ⚑ `FiledBy` IS THE ONE FIELD THE CALLER CANNOT CHOOSE. `Session.CurrentUser` is whoever presented the
  //    credential — the scanner here, the courier when a courier files their own — so the audit answer to
  //    "who did this?" cannot be forged by the request that asks the question.
  new Handover { Courier = courier, FiledBy = Session.CurrentUser, Parcel = parcel };
}
```

1. **The actor is never a parameter.** `FiledBy = Session.CurrentUser` is the only honest stamp, and it is why a
   record filed by a device is still attributable to that device afterwards. A `filedBy` argument would be a lie the
   caller writes.
2. **A named subject is not a permitted subject.** The `RoleGrant.Any(…)` check is what stops the terminal in the
   hallway filing against an account that merely exists. Choose the predicate your domain actually means — "holds
   the Courier role", "is on this depot's roster", "has an open assignment".
3. **The refusal reaches the caller as a status, not a crash.** A `ValidationException` is a `400` and a
   `NotFoundException` a `404`, with your wording — see [[api-rest#who-is-user]].

### Publishing it, and which credential the device presents   {#the-route}
The device is authenticated exactly like a person, so the API needs no special mode:

```osy title="the published route — an ordinary API-key API" test app=security-acting-for-another-principal
[AuthMethod]
string Login(string email, string password) {
  var a = Account.Where(x => x.Email == email).FirstOrDefault();
  if (a == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, a.PasswordHash)) { return Security.IssueJwt(a.Id, a.Email); }
  return "";
}

app.AuthBootstrap = new AuthBootstrap { Role = StaffRole.Authenticator, Login = Login };

app.Apis = [
  new RestApi("Depot") {
    Route = "depot",
    Auth  = new ApiAuth { ApiKey = true },
    Endpoints = [ new Endpoint(RecordHandover) { Method = HttpMethod.Post, Path = "/handover" } ],
  },
];
```

A key belongs to a person or to a device with equal ease, so a device on a wall and a nightly job on a server are the
same story:

| the caller | the credential it presents | what `user` is |
|---|---|---|
| a shared terminal | `X-API-Key: pk_…`, minted for the terminal's own account | the terminal |
| a back-office job | the same, minted for the job's own account | the job |
| a person, signed in | their bearer token from `Login` | that person |

### How the first role is minted when the app has no admin UI   {#minting-the-role}
This is the half that has no answer inside the app, and it is where the design usually stalls: the `Scanner` grant
has to exist before the scanner can do anything, and a brand-new deployment has no screen for creating it.

**It is not created in the app. It is created from the CLI, and the verb writes both rows.**

```bash
# the principal row AND its RoleGrant row, in one act. --role is REQUIRED (repeatable).
osy user add scanner@depot.example --role Scanner --password 'a-long-one'

# …and the credential it will present, printed ONCE — store it on the device
osy user apikey generate scanner@depot.example
```

Against a deployed platform the same two acts are `osyrin app user add …` and
`osyrin app user apikey generate …` — the same store, the same flags. The full flag set is
[Adding an account](https://osysharp.com/reference/local/adding-an-account/).

⛔ **`osy user add` refuses unless the app declares `app.Auth`**, and this is the single most common blocker. It is a
DIFFERENT declaration from `app.AuthBootstrap`: the bootstrap names the *functions* your login page calls, which is
all that page needs; `app.Auth` names the **fields** that hold the login and the hash, which is what anything OUTSIDE
the app needs, because it has no page to post to. Declare both.

```osy title="the two lines that let an operator create an account" test app=security-acting-for-another-principal
// `app.AuthBootstrap` (above) is how the app's OWN login page signs somebody in.
// `app.Auth` is how anything outside the app does — the CLI, the console, `--as`.
app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };

// A credential not named here is written to the entity-change audit trail VERBATIM.
app.Audit = new AuditConfig {
  Redact = new AuditRedaction {
    Properties = [Account.PasswordHash, Account.ApiKeyHash, Account.ApiKeyHash2],
  },
};
```

Three further facts about that bootstrap, each of which has cost somebody a design:

- **`--role` is required, not optional.** An account with no role holds no authority your `security {}` can act on,
  and an omitted `--role` is far more often a forgotten flag than an intent. `--via-signup` is the alternative: it
  runs the app's own `Signup` and lets that decide what it grants.
- **A `User.Count() == 0` first-admin gate in `Signup` is closed by the FIRST row added by ANY means** — a browser
  signup, `osy user add`, `osy import`. It is a real pattern and a fragile one; prefer minting the privileged
  account with `--role` and leaving `Signup` to grant only the ordinary role.
- **`app.AuthBootstrap`'s ephemeral principal is NOT "acting for" anyone.** It is a role with *no user at all*,
  armed by the engine only for the functions the bootstrap declares, so that a login can read a row before anybody
  is authenticated. It cannot be borrowed for this. See [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/).

### The lint that catches the race   {#the-lint}
`osy lint` reports `security-integration-role-granted-by-signup-order` (SHOULD) when the two facts above meet in one
app: a role decided by **how many rows already exist**, in an app that mints **per-user API keys**.

```osy title="the shape the lint fires on — a machine's role decided by whoever signs up first" syntax
[AuthMethod]
string Signup(string email, string password) {
  bool isFirst = User.Count() == 0;                                  // ⛔ the ordinal test
  var u = new User { Email = email, PasswordHash = Security.HashPassword(password) };
  new RoleGrant { Grantee = u, Level = isFirst ? StaffRole.Scanner   // ⛔ the machine's role, by race
                                               : StaffRole.Courier };
  return Security.IssueJwt(u.Id, u.Email);
}

app.Apis = [ new RestApi("Depot") { Auth = new ApiAuth { ApiKey = true }, … } ];   // ⛔ …and machine accounts exist
```

**Why the conjunction, and not the count alone.** "The first account to sign up becomes the ADMIN" is a real pattern
people deliberately choose, and the bullet above calls it fragile rather than wrong. What makes the shape above
different is that the intended holder **is not somebody who signs up at all** — it is a terminal on a wall. An API
key is minted against one `[Principal]` row from the CLI ([[api-rest#who-is-user]]), so the count can never reach the
device; it can only reach whoever loads your public signup page first, in a window that is open from deploy until the
first registration. The author already holds the tool that provisions it, which is why the finding costs nothing to
act on.

**What to write instead** — the four acts, in order:

```bash
# 1 + 2 — `osy user add` REFUSES without `app.Auth` (it names the FIELDS; `app.AuthBootstrap` names the FUNCTIONS)
osy user add scanner@depot.example --role Scanner --password 'a-long-one'
# 3 — the credential the device presents, printed ONCE
osy user apikey generate scanner@depot.example
```
```osy title="…and Signup stops deciding, so there is no race left to lose" syntax
// 4 — and `Signup` stops deciding: everybody who signs up is a courier
new RoleGrant { Grantee = u, Level = StaffRole.Courier };
```

The rule is SHOULD rather than MUST because the decisive fact — that this role is a *machine's* — is inferred from
the app minting API keys, not proven of that role. An author who really does mean the first signup to hold it says
so where it fires: `[SuppressWarning("security-integration-role-granted-by-signup-order")]` on the function.

### Four things that look like this and are not   {#not-this}
| | what it is | may I use it here? |
|---|---|---|
| `runas (P) { … }` | rebinds the acting principal inside a `[Test]` | **no** — a compile error outside a test |
| `Api.KeyFor(P)` / `Ui.SignInAs(P)` | mint a working credential for a principal in a `[Test]` | **no** — same refusal, same reason |
| `osy run --as` / `osy import --as` | run a function or an import AS a real user | only as tooling, and it takes **that user's own password** — naming a principal never makes you one |
| the bootstrap's ephemeral principal | a role with no user, for the declared auth methods only | **no** — it is not anybody, so it cannot be somebody |

⚑ The pattern in all four: **presenting a credential is the only way to be somebody.** The three test-only verbs are
allowed to break that precisely because a test's whole world is disposable; production has no equivalent, by design.

## See also       {#see-also}
- [publishing a REST API (app.Apis)](https://osysharp.com/reference/api/rest/) — `app.Apis`, `ApiAuth`, and the table of who `user` is on an authenticated API call
- [Adding an account](https://osysharp.com/reference/local/adding-an-account/) — every flag of `osy user add`, and its `osyrin app user add` twin
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — the grant table and the `when` predicates the roles above are written with
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — the ephemeral auth principal, and why it is not an "act as" mechanism
- [runas](https://osysharp.com/reference/testing/runas/) — proving the rules above deny the callers they should
- [Running a function](https://osysharp.com/reference/local/running-a-function/) — `osy run --as`, and why it asks for the principal's own password


---

<!-- https://osysharp.com/reference/security/password-auth/ -->

# app.Auth — how the platform authenticates a user of your app

> `app.Auth` binds two properties of your `[Principal]` — which one is the login, which one holds the password hash — and with that the platform can create and authenticate a user of your app **without you writing any code**: it hashes, verifies and issues the ticket itself. It is what the tooling (provisioning a first user, the console login) runs on. It is NOT what your own `[AuthMethod] Login` uses — that function does its own verifying — and knowing which is which is the difference between two auth surfaces and one confusing one.

<!-- id: security-password-auth · area: security · stability: stable · html: https://osysharp.com/reference/security/password-auth/ -->

## Summary        {#summary}
**`app.Auth`** declares how a user of your app is authenticated **generically** — by the platform, with no code from
you. For a password, you bind two properties of your [[security-entity-security|`[Principal]`]] entity: the one that
carries the login, and the one that stores the hash.

```osy title="bind the login and the hash — that is the whole declaration" test app=security-password-auth
[Principal]
entity User {
  // [Unique] or two rows share a login and WHICH account you sign into is undefined — and no check-then-insert in
  // application code can close that race, because the race is in the database.
  [Required, Unique, MaxLength(255)] string Email;
  [MaxLength(200)] string PasswordHash;
  security {
    allow read when IsAuthenticated;
    allow read where Id == user.Id;
    // A read grant is a ROW grant, so without this the hash rides out with the row — to an admin listing users,
    // to an export, to the user's own page.
    //
    // CONDITIONED, and the condition is the sign-in itself. A masked column is DROPPED from the read, so an
    // unconditional deny would hide the hash from the verification too and refuse a correct password exactly as it
    // refuses a wrong one. Sign-in happens while you are still ANONYMOUS — and an anonymous caller already gets no
    // rows here, so this one line says "verify me, then never show me" with nothing else to declare.
    deny read PasswordHash when IsAuthenticated;
  }
}

app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };
```

That is enough for the platform to **create** a user of your app and **authenticate** one: it hashes the password,
verifies it, and issues the ticket, reading and writing the fields you named. The field names come from this
declaration — nothing about `Email` or `PasswordHash` is hardcoded, and your properties may be called anything.

## Signature      {#signature}
```osy syntax
app.Auth = new PasswordAuth { LoginField = <PrincipalProp>, PasswordField = <PrincipalProp> };
app.Auth = new OAuthAuth { Client = <OAuthClient> };
app.Auth = [ new PasswordAuth { … }, new OAuthAuth { … } ];   // several methods, offered together
```

`LoginField` and `PasswordField` are both **required**, and each must **name a property of the `[Principal]` entity**
(a bare name, not a string) — an unknown name is a compile error, so the binding cannot rot when you rename a
property. `app.Auth` is a **singleton**: declare it once. Its value may be a single method or a **list**, when an app
offers more than one way to sign in.

## Description    {#description}

### `app.Auth` and `[AuthMethod]` are two different doors   {#two-doors}
This is the distinction to get right, and the one that reads as confusing until you see it:

| | **`app.Auth`** (`PasswordAuth`) | **[[security-auth-method|`[AuthMethod]`]]** + [`app.AuthBootstrap`](https://osysharp.com/reference/security/auth-bootstrap/) |
|---|---|---|
| **Who runs the sign-in** | the **platform**, generically | **your function**, written in Osy# |
| **You write** | two field bindings | a `Login` function (and usually a `Signup`) |
| **Who calls it** | the tooling — provisioning a user, the console/API login | your app's own login page |
| **Hashing / verifying / the ticket** | the platform does it | you do it, with [`Security.*`](https://osysharp.com/reference/stdlib/security/) |
| **Custom rules** (invite-gated signup, a first-admin grant, a lockout) | not possible — it is generic by design | anything you can write |

They are **not alternatives to choose between**. A real app usually declares **both**, and for good reason: `app.Auth`
gives the platform a way to create and authenticate a user of your app before you have built any of it (and keeps
`osy`-side tooling working forever after), while your `[AuthMethod] Login` is the sign-in your users actually see,
where you control the flow. The platform's own Admin app — the most complete app we have — declares both.

Concretely: your `[AuthMethod] Login` does *not* consult `app.Auth`. It reads the user, calls
`Security.VerifyPassword`, and returns `Security.IssueJwt(...)` itself. Nothing is shared between the two paths except
the rows in your `[Principal]` table — which is exactly why they interoperate: a user the platform created with
`app.Auth` can sign in through your login page, and a user your `Signup` created can be authenticated by the tooling.
**Both hash passwords the same way**, so the hash column is meaningful to both.

### Why the platform needs the binding at all   {#why-binding}
The platform does not know what your user entity looks like. It cannot assume a property called `Email`, or that the
hash lives in `PasswordHash` — your app might have `Username` and `Secret`, or a Norwegian app might call it
`Epost`. `app.Auth` is the two-line answer to "which column do I compare, and which one do I hash into", and it is
compile-checked against the `[Principal]`, so it cannot drift out of step with the entity it names.

Declare no `app.Auth`, and the generic path simply reports that the app declares no password method — the tooling
cannot provision or authenticate a user. Your own `[AuthMethod] Login` still works, because it never needed it.

### Offering OAuth instead of, or beside, a password   {#oauth}
`new OAuthAuth { Client = <name> }` authenticates against an OAuth client you declared in `app.OAuthClients`, instead
of (or alongside) a password. Give a list to `app.Auth` when an app offers both, and the caller picks:

```osy title="offer a password AND an OAuth provider" syntax
app.Auth = [
  new PasswordAuth { LoginField = Email, PasswordField = PasswordHash },
  new OAuthAuth { Client = Google },
];
```

## Examples       {#examples}
The full shape, as a real app declares it — the generic binding *and* the app's own login, side by side:

```osy title="both doors, in one app" test app=security-password-auth-both-doors
[Role] enum AppRole { Authenticator, Member }

entity RoleGrant {
  [Required] User User;
  [Required] AppRole Role = AppRole.Member;
  security { allow create when IsAuthenticator; }
}

policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);

[Principal]
entity User {
  [Required, Unique, MaxLength(255)] string Email;
  [MaxLength(200)] string PasswordHash;
  security {
    allow read when IsAuthenticated;
    // ⚑ BY ROLE, not by row. `Login` runs as the armed AuthBootstrap principal, which holds no row of its own —
    //   so a `where` filter answers it with nothing and the login can never find the account it was called to
    //   check. A role predicate is answerable with no rows at all, which is why it works where a filter cannot.
    allow read when IsAuthenticator;
    // ⚑ AND THE CONDITION NAMES THAT SAME ROLE. With a `[AuthMethod]` login the sign-in is NOT anonymous (it is
    //   that armed principal), so the `when IsAuthenticated` spelling at the top of this page — right for a
    //   PasswordAuth-only app — would fire during login here and refuse every correct password, silently.
    deny read PasswordHash when !IsAuthenticator;
  }
}

// The app's OWN login — it verifies and issues the ticket itself; app.Auth is not involved.
[AuthMethod]
string Login(string email, string password) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  // ⚑ SPEND THE SAME TIME EITHER WAY. Returning early on "no such account" makes the refusal FASTER than a wrong
  //   password, and that difference is an account-enumeration oracle anyone can time. The one-argument
  //   `VerifyPassword` verifies against nothing, costs a full KDF, and answers false.
  if (u == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); }
  return "";
}

app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };
app.AuthBootstrap = new AuthBootstrap { Role = AppRole.Authenticator, Login = Login };
```

The two declarations coexist and neither knows about the other. What DOES differ from the summary fence at the top of
this page is the `security {}` block, and the difference is not cosmetic: an app whose only door is `app.Auth` signs
you in while you are still **anonymous**, so `deny read PasswordHash when IsAuthenticated;` never fires during
verification. An `[AuthMethod]` login runs as the principal `app.AuthBootstrap` arms — which IS authenticated — so
here the same line would refuse every correct password. Condition on the auth ROLE, as above, and both doors work.

## See also       {#see-also}
- [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/) — your own login function, and why it needs no `app.Auth`
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — the wiring that lets a signed-out visitor reach that function
- [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/) — the hashing, verifying and ticket-issuing your own login does by hand
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `[Principal]` entity whose fields this binds


---

<!-- https://osysharp.com/reference/security/auth-bootstrap/ -->

# auth bootstrap (login, before anyone is signed in)

> Under deny-all, login faces a paradox: it must read a user row *before* anyone is authenticated. `app.AuthBootstrap` resolves it. You name a `[Role]`, and the engine runs your `[AuthMethod]` functions as an ephemeral principal bearing that role — with no `User` row and no grant row behind it, so a `RoleGrant.Any(g => g.User == user …)` policy is TRUE for it without any grant existing — but only for a caller who is not already signed in. It mints no special access: what that principal may touch is your ordinary `security { }` grants for the role. The bootstrap path is therefore not a hole you have to remember; it is leashed by rules you can read on the entity.

<!-- id: security-auth-bootstrap · area: security · stability: stable · html: https://osysharp.com/reference/security/auth-bootstrap/ -->

## Summary        {#summary}
With [deny-all](https://osysharp.com/reference/security/secure-by-default/) in force, an entity you have not granted is denied to everyone — and that
breaks login, which must reach a user row *before* anyone is signed in. **`app.AuthBootstrap`** resolves the paradox by
declaring the sign-in flow, so the engine can run it under an identity you defined:

```osy title="the whole wiring" test app=security-auth-bootstrap
app.AuthBootstrap = new AuthBootstrap {
  Role          = AppRole.Authenticator,   // the role the ephemeral principal bears
  Login         = Login,                   // an [AuthMethod] — run as that principal, when nobody is signed in
  Signup        = Signup,                  // optional
  PasswordReset = StartReset,              // optional
  LoginPage     = LoginPage,               // optional — where a signed-out visitor is sent
};
```

The ephemeral principal has **a role and no user**. It is minted by the engine, never by your code, and only for a
caller with no ticket. It gets **no special access**: it may touch exactly what your `security { }` blocks grant that
role — see [the leash](https://osysharp.com/reference/security/auth-method/).

## Signature      {#signature}
```osy syntax
app.AuthBootstrap = new AuthBootstrap {
  Role          = <RoleEnum>.<Member>,   // REQUIRED — a member of the app's [Role] enum
  Login         = <function>,            // REQUIRED — an [AuthMethod] function
  Signup        = <function>,            // optional  — an [AuthMethod] function
  PasswordReset = <function>,            // optional  — an [AuthMethod] function
  LoginPage     = <component>,           // optional  — a routed [Page] component
  OAuthSignup   = <function>,            // optional  — an [AuthMethod]: finish a new OAuth sign-in
  OAuthLink     = <function>,            // optional  — an [AuthMethod]: link a provider to an existing account
  AcceptInvite  = <function>,            // optional  — an [AuthMethod]: accept an invite (token + credentials)
};
```

All eight members, and nothing else — an unknown member is a compile error. `Role` and `Login` are **required**; the
rest are optional. `app.AuthBootstrap` is a **singleton**: declaring it twice is a compile error. Every named function,
the `Role` member and `LoginPage` are resolved and checked at compile time, so a typo is a build failure and never a
door left open at runtime. The two `OAuth*` slots wire your own OAuth-completion pages — see [OAuth account completion — Security.VerifyPendingOAuthEmail / LinkOAuthFromPending](https://osysharp.com/reference/security/oauth-completion/)
for what they do — and `AcceptInvite` wires an invite-acceptance flow ([below](#accept-invite)).

## Description    {#description}

### Why can't `Login` read the `User` row?   {#paradox}
Deny-all is the right posture: an entity nobody granted is readable by nobody. But `Login` has to read the `User` row
to check a password, and at that moment there is **no signed-in user to grant anything to**. Simply marking `Login` as
"anonymous" would not help — an anonymous caller is granted nothing, so the read inside it would still be denied.

The missing piece is not reachability, it is **identity**. The engine needs an identity to run the sign-in flow *as* —
one you have described, and can grant to, like any other. That identity is the ephemeral principal.

### Who does the engine run `Login` as? The ephemeral principal   {#ephemeral-principal}
When a caller with **no ticket** invokes one of the declared methods, the engine runs it as a principal that:

- **bears the one `[Role]` you named** (`Role = AppRole.Authenticator`), and
- **has no user behind it** — there is no `User` row, and no grant row, for the authenticator. It is not an account;
  it is an identity that exists for the duration of the call.

Three properties follow, and together they are what make the scary corner safe:

- **Only a declared method is routed.** Any other function invoked anonymously gets the ordinary anonymous context —
  deny-all — and never the role. The method list is the one in `app.AuthBootstrap`, not a marker a stray function
  could wear (that is the other half of [the both-ways check](https://osysharp.com/reference/security/auth-method/)).
- **Only when unauthenticated.** A caller who is already signed in and invokes `Login` is **not** elevated; they run
  as themselves. So no ordinary user can use the sign-in path to borrow the auth role.
- **Least privilege, by your own grants.** The principal may read and write exactly what your `security { }` blocks
  grant its role. Grant it nothing and it can do nothing. There is no source-level construct that mints a
  role-bearing principal, so the elevation seam lives inside the engine, where your code cannot reach it.

### Granting the auth role — it must be a `when`, in the role-grant shape   {#when-not-where}
This is the one rule that will catch you out, and the reason is worth understanding — it follows from the ephemeral
principal having **no user**.

A grant to the auth role must be a **`when` guard** naming a **role** policy — never a `where` filter, and never a
policy that goes looking for a row belonging to the caller:

```osy syntax
allow read  when IsAuthenticator;    // ✅ a fact about the PRINCIPAL's ROLE — true for the ephemeral one
allow read  where User == user;      // ❌ compares a column to the caller's user id — and there ISN'T a user
```

A **`when IsAuthenticator`** guard is answered from **the role the principal bears** — no row is consulted, so it is
true for a principal that has no rows at all. Anything that has to *find a row for this user* — a `where` filter, or a
policy that hops into a membership table — resolves the caller's user id, which for the ephemeral principal is
nothing. It matches no rows, and denies. That is correct (a caller who is nobody owns nothing), but it means the auth
role cannot be granted that way.

So: **grant the auth role by role; grant everyone else however you like.** The row filters stay for real, signed-in
users, where they work perfectly.

**And it is every operation the flow performs, not just `read`.** `Login` reads. `Signup` also *creates* — the
principal row, and usually a role grant. `AcceptInvite` *updates* the invite it just consumed, and *reads* the
membership table to stay idempotent. Each of those needs its own grant to the auth role, and each fails its own way:
a missing `create` refuses the signup for everyone; a missing `update` refuses the write and takes the whole flow down
with it; a missing `read` answers "nothing found", so the check silently stops being made.

⚑ A column written on a row the flow **just created** is covered by `allow create` — it is not an `update` until the
row exists. That is why stamping a last-login timestamp needs `allow update LastLoginAt when IsAuthenticator;` for the
sign-IN path and nothing extra for the sign-UP path, from the same line of source.

### How must the role policy be written?   {#policy-shape}
For `when IsAuthenticator` to be answerable from the role alone, the policy must be written in the **role-grant
shape**: an existence check over a grant entity, correlating the principal and comparing the role column to a member
of your `[Role]` enum. That shape is recognised and answered from the principal's roles — which is exactly why it is
true both for a real admin (who holds a grant row) and for the ephemeral authenticator (who holds only a minted role).

```osy title="the shape the engine recognises" test app=security-auth-bootstrap
[Role] enum AppRole { Authenticator, Member, Admin }

[Principal]
entity User {
  [Required, Unique, MaxLength(255)] string Email;
  [MaxLength(200)] string PasswordHash;
  [MaxLength(64)] string ResetToken = "";          // where a reset in flight is recorded — see StartReset below
  security {
    allow read   when IsAuthenticator;             // ← true for the ephemeral principal
    allow create when IsAuthenticator;
    allow update when IsAuthenticator;             // ← a reset writes the row it just looked up
    allow read where Id == user.Id;                // ← for real users, by row
    deny read PasswordHash when !IsAuthenticator;  // the hash is the auth flow's alone
    deny read ResetToken   when !IsAuthenticator;  // …and so is a live reset token
  }
}

entity RoleGrant {
  [Required] User User;                            // the principal reference…
  [Required] AppRole Role = AppRole.Member;        // …and the [Role] enum column: this is a role-grant entity
  security {
    allow read   when IsAdmin;
    allow create when IsAuthenticator;             // signup may mint one — see the role-grants page
  }
}

policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);
policy IsAdmin         => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Admin);
```

The same policy serves both kinds of caller, which is the elegance of it: for a **real** user it means "you hold a
grant row saying Admin"; for the **ephemeral** principal it means "the role you were minted with is Authenticator".
You write one sentence, and it is true in both worlds.

**Other tiers need not be roles.** A policy that is *not* a role-grant — say `IsOrgAdmin`, an existence check over a
membership table whose enum is an ordinary domain enum — is a perfectly good `when` guard and a perfectly good
[[ui-authorize|`[Authorize]`]] policy. It is simply answered a different way: by looking for the row. See
[role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/), which is where the two tiers are laid out side by side. The only caller such a policy can
never be true for is the ephemeral one — because it has no rows.

### If nothing tests the role, the role decides nothing   {#role-tests-nothing}
`Role` is the ephemeral principal's **only** property — it is minted with no user, so "I hold this role" is the whole
of what it can say about itself. A role makes no difference to anything until a guard tests for it, so an app that
declares `Role = AppRole.Authenticator` and then never mentions `Authenticator` in a `policy` body or in any
`security` rule's `when` has armed a principal that can prove nothing. Both grants the flow depends on become
unwritable at once:

- **the grant.** `when IsAuthenticated` is FALSE for this principal (it has no user) and a `where` row-filter has no
  user id to match, so with no role predicate available the only grants that admit the login also admit everyone.
- **the mask.** `deny read PasswordHash when !IsAuthenticator;` is how the hash is hidden from every reader *but* the
  login. With no policy to negate, the two things left are both wrong: an unconditional `deny read PasswordHash;`
  drops the column from the login's own `SELECT`, so every correct password is refused; no `deny read` at all ships
  the hash to the browser inside `Session.CurrentUser`.

None of that is a compile error — the app builds, and signing **up** may work perfectly, which is what hides it. The
maturity rule `security-auth-role-tests-nothing` (MUST) reports it, and its remediation carries the missing
declarations written in your app's own names:

```osy title="the role is armed and nothing tests it" syntax
[Role] enum AppRole { Authenticator, Member }

[Principal]
entity User {
  [Required, Unique, MaxLength(200)] string Email;
  [MaxLength(200)] string PasswordHash;
  security { allow read, create when IsAuthenticated; }   // ← never true for the auth flow
}
// …and no policy anywhere mentions AppRole.Authenticator

app.AuthBootstrap = new AuthBootstrap { Role = AppRole.Authenticator, Login = Login, Signup = Signup };
```

The fix is the grant entity plus the policy that reads it — the [role-grant shape](https://osysharp.com/reference/security/role-grants/) above —
after which `when IsAuthenticator` is answerable from the role alone.

⚠ **The role may be tested inline**, without a named policy in between: `allow read when RoleGrant.Any(g => g.Grantee
== user && g.Level == AppRole.Authenticator);` is the same claim, the engine answers it the same way, and the rule
stays silent on it. What it reports is a role no guard in the app names at all.

### `LoginPage` — where a signed-out visitor gets sent   {#login-page}
`LoginPage` names your app's login `[Page]` component, so the platform knows **where to send** a signed-out visitor
who lands on a protected page — your declared route, rather than a guess. It carries no authority; it is a route.
Omit it and the client falls back to `/login`.

### There is no signup PAGE slot — that one is on you   {#no-signup-page}

`AuthBootstrap` names a `Signup` **function** and a `LoginPage` **component**. It never names a signup *page*, so
nothing in the language asks for one — and an app whose only way in is a login form nobody can get an account from is
locked, on a clean compile, with every test green.

⚠ **Measured 2026-08-18**, on an app built from the shape of this page: `Signup` and `Login` were written as
`[AuthMethod]`s, `LoginPage` was wired, and there was **no page anywhere that creates an account**. Write the
`[Page("/signup")]` yourself, and link to it from the login page.

### Prove the ROUND TRIP, not the signup   {#round-trip}

Signing up leaves you signed in, so a test that stops there passes while signing back in is still broken — the two
paths share almost nothing but the entity. Drive all three:

```osy syntax
Ui.Visit("/signup");
Ui.Fill("Email", "sam@example.com");
Ui.Fill("Password", "correct horse battery staple");
Ui.Click("Create account");
Assert.OnPage("/");

Ui.SignOut();
Ui.Visit("/login");
Ui.Fill("Email", "sam@example.com");
Ui.Fill("Password", "correct horse battery staple");
Ui.Click("Sign in");
Assert.OnPage("/");        // ⛔ the assertion the signup-only test never makes
```

⚠ **Measured the same day:** signing up got the person in, signing back in did not, and the app's own two auth tests
were `[Skip]`ped — so nothing said so. A skipped auth test is worse than none: it reads as coverage.

### `OAuthSignup` / `OAuthLink` — someone signs in with a provider and has no local account yet   {#oauth-slots}
The two `OAuth*` slots wire the pages that finish an **OAuth sign-in** — the case where a visitor signs in with a
provider (Google, …) but has no account yet, or a local account that is not yet linked. Each names an `[AuthMethod]`,
armed with the same ephemeral role as `Login`/`Signup` (so what it may write is exactly your `security { }` grants for
that role, nothing more):

- **`OAuthSignup`** — the function your **new-account** completion page calls: it reads the provider-verified email out
  of the sealed pending token, creates the account, records the identity link, and returns the ticket.
- **`OAuthLink`** — the function your **link-confirm** page calls, when the email already belongs to a local account:
  it attaches the provider identity to that account so next time the provider signs them straight in.

The mechanics — how the email is sealed into the token so the browser cannot forge it, and the two server calls
`Security.VerifyPendingOAuthEmail` / `Security.LinkOAuthFromPending` — live on their own page: see
[OAuth account completion — Security.VerifyPendingOAuthEmail / LinkOAuthFromPending](https://osysharp.com/reference/security/oauth-completion/). Both slots are optional; omit them and the app simply offers no OAuth sign-in.

### `AcceptInvite` — an already-invited teammate turns their token into an account   {#accept-invite}
`AcceptInvite` wires the flow where an **already-invited** person joins — a teammate you emailed an invite link, not a
stranger signing themselves up. The invitee arrives **anonymous**, carrying an invite token, and your accept function
turns that into an account and a membership. It is the same shape as the OAuth slots — a foreign principal presenting a
credential — so it is armed with the same ephemeral role as `Login`/`Signup`, and what it may write is exactly your
`security { }` grants for that role, nothing more:

- **`AcceptInvite`** — the `[AuthMethod]` your **invite-accept** page calls. It takes the invite token plus whatever the
  new user supplies (a password, a display name), and its body does the leashing: it **validates the token** (that it
  exists, is unspent, and is not expired), provisions or authenticates the user, records their org membership, and
  returns the ticket from [`Security.IssueJwt`](https://osysharp.com/reference/stdlib/security/). An invalid or spent token returns `""` — no ticket,
  no account.

The token check is the whole security boundary — the ephemeral role lets the function *reach* the invite and membership
rows, and the function's own validation is what decides whether this caller may have them. It is optional; omit it and
the app simply offers no invite-acceptance flow.

### What does `Login` return, and what does the page do with it?   {#ticket}
Your `Login`/`Signup` returns the session ticket from [`Security.IssueJwt(userId, email)`](https://osysharp.com/reference/stdlib/security/). The login
page hands it to `Session.SignIn(ticket)`, which stores it as the session bearer — so the *next* request is
authenticated as that user, with their real grants, and the ephemeral principal is gone. A failed sign-in returns
`""`, `Session.SignIn` stores nothing, and the visitor stays anonymous.

### Signing out   {#sign-out}
`Session.SignOut()` is the mirror of `Session.SignIn` — it **drops the stored session ticket and returns to the login
page**. There is no server round trip: the ticket lives in the browser and the server keeps no session to invalidate
(every request re-verifies the bearer, and a dropped bearer is simply anonymous). It takes no arguments and, like
`Session.SignIn`, runs in-process on the client, so it belongs in an `action` body wired to a control's `onClick`:

```osy syntax
action SignOut() { Session.SignOut(); }
// …
Pressable(onClick: SignOut) { Text("Sign out"); }
```

### Who is signed in? `Session.CurrentUser`   {#current-user}
`Session.CurrentUser` reads the **current authenticated principal** as your app's `[Principal]` entity — `var me =
Session.CurrentUser;` then `me?.DisplayName`, `me?.Email` shows who is signed in (a shell footer, a "my account" page).
It is RLS-scoped (the caller reads their own row), returns **null for an anonymous session** (or while the row loads),
and requires the app to declare a `[Principal]` entity (else it is a compile error). Under the hood it is an ordinary
data read — `<Principal>.SingleOrDefault(p => p.Id == <the current principal>)` — no round-trip you wouldn't already pay
for a query, and it reflects edits to the row like any other read.

```osy syntax
[Layout] component Shell() {
  var me = Session.CurrentUser;
  render { Text(me?.DisplayName ?? me?.Email ?? "Signed out"); }
}
```

## Examples       {#examples}
The complete flow — the two auth methods, the page, and the wiring:

```osy title="login, signup, reset, and the page they sign you in from" test app=security-auth-bootstrap
// The delivery seam a reset needs. The platform has no `Email.Send`: an outbound provider is something the app
// DECLARES, once, with its url, its auth and the secret that authenticates it. See [a typed HTTP client (client)](https://osysharp.com/reference/http/client/).
app.Secrets = [ new Secret("MailerKey") ];

class MailInput { public string To; public string Subject; public string? Text; }
class MailResult { [ExternalName("id")] public string? MessageId; }

client Mailer {
  BaseUrl = "https://api.mail.example";
  Auth    = new BearerAuth { Secret = Secret.MailerKey };
  [Post("/send")] MailResult Send(MailInput body);
}

[AuthMethod]
string Login(string email, string password) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  // Spend the same time either way — an early return makes "no such account" measurably faster
  // than "wrong password", and that difference is an enumeration oracle anyone can time. The
  // one-argument form verifies against nothing, costs a full KDF, and answers false.
  if (u == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); }
  return "";
}

[AuthMethod]
string Signup(string email, string password) {
  var u = new User { Email = email, PasswordHash = Security.HashPassword(password) };
  var grant = new RoleGrant { User = u, Role = AppRole.Member };
  return Security.IssueJwt(u.Id, u.Email);
}

// A reset has to MINT, STORE and SEND, and it is the third that gets left out. The token's whole job is to reach a
// mailbox only the account's owner reads; a flow that mints one and stops leaves the confirm step asking for a
// value nobody can obtain. `osy lint` reports that as `security-reset-token-never-delivered`, at MUST tier.
[AuthMethod]
string StartReset(string email) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  if (u == null) { return ""; }        // say nothing either way — an unknown email is not news for a stranger
  var minted = Security.RandomId(32);
  u.ResetToken = minted;
  // Caught, so the mint survives a failed send: the call throws and a function commits at the END, so an uncaught
  // failure rolls the token back too — no mail, no token, and a page that looked like it worked.
  try { Mailer.Send(new MailInput { To = email, Subject = "Reset your password", Text = "Your code: " + minted }); }
  catch (Exception e) { Log.Error(e, "reset for {Email} was minted but could not be sent", email); }
  return "";                           // ⛔ NEVER the token. Handing it back lets anyone who knows your address take
}                                      //    the account — which is what the out-of-band channel exists to prevent.

[Page("/login")]
[AllowAnonymous]
[Render(CSR)]
component LoginPage() {
  string email = "";
  string password = "";
  action SignIn() { Session.SignIn(Login(email, password)); }
  action Register() { Session.SignIn(Signup(email, password)); }
  render {
    Input(value: email, placeholder: "Email");
    Input(value: password, placeholder: "Password", type: "password");
    Button("Sign in", onPress: SignIn);
    Button("Create account", onPress: Register);
  }
}
```

Read that against the grants above and the whole system is visible at once: `Login` can read `User` — and the hash,
which the field mask denies to everyone else — because `Authenticator` was granted a read. It cannot read anything
else, because nothing else granted it. Delete the `allow read when IsAuthenticator` line and login stops working;
nothing else in the app changes. That is what it means for the bootstrap to be leashed by ordinary rules.

## See also       {#see-also}
- [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/) — the `[AuthMethod]` marker each wired function must carry, and the leash in detail
- [OAuth account completion — Security.VerifyPendingOAuthEmail / LinkOAuthFromPending](https://osysharp.com/reference/security/oauth-completion/) — the `OAuthSignup`/`OAuthLink` slots in full: sealed pending tokens and the two server calls
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — how the first account becomes an admin without opening a self-elevation path
- [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/) — `HashPassword` · `VerifyPassword` · `IssueJwt` · `RandomId`
- [app.Auth — how the platform authenticates a user of your app](https://osysharp.com/reference/security/password-auth/) — `app.Auth`, the code-free way the platform can authenticate a user of your app
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — the deny-all posture the bootstrap unblocks
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` grants that leash the ephemeral principal
- [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) — page-level authorization, the request-time complement


---

<!-- https://osysharp.com/reference/security/capability-row-ownership/ -->

# 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 capabilities scope every read and write to your app's `[Principal]` automatically, with no rule for you to write. The one thing they need from you is a `[Principal]` entity to scope to: an app that imports such a capability and declares none is a compile error, because a row owned by a user has no meaning in an app with no users.

<!-- id: security-capability-row-ownership · area: security · stability: stable · html: https://osysharp.com/reference/security/capability-row-ownership/ -->

## Summary        {#summary}
A capability can ship tables whose rows belong to **one signed-in user**. Conversation memory is the clearest case:
a chat session is *yours*, and another user of the same app must not be able to list or open it.

Those capabilities carry that rule themselves. You do not write it, you cannot forget it, and it holds wherever the
read comes from — a page, a function, an MCP tool. What the capability cannot supply is **who the users are**: that
is your app's `[Principal]` entity. So the one requirement is that you declare one.

```osy syntax
using Osysharp.Agents;

[Principal] entity User { string Email; }     // ← the only thing the capability needs from you

// Nothing else. Every ChatSession read and write is already scoped to the signed-in user.
```

## Signature      {#signature}
```osy syntax
using Osysharp.Agents;                     // a capability whose rows are owner-scoped
[Principal] entity User { … }                 // REQUIRED — the type its rows are scoped to
```

Importing such a capability without a `[Principal]` is refused at compile time, naming the capability and both ways
out:

```text
`using Osysharp.Agents;` brings in 'ChatSession', whose `UserId` belongs to a signed-in user — and this app
declares no `[Principal]` entity, so there is no user for it to belong to. Either declare one
(`[Principal] entity User { … }`), or drop `using Osysharp.Agents;`.
```

## Description    {#description}
Under [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) a table nobody has granted access to is denied to everyone. A capability's
owner-scoped tables come with their grant already written — *this row is reachable by the principal it belongs to* —
so opting in gives you a working, private store rather than a locked one.

Three consequences worth knowing:

- **The rule is on the entity, not on the route.** It is enforced by the read engine, so a conversation you do not
  own is absent from a query, not merely hidden by whichever endpoint you went through. Reaching the same table from
  a function or a tool gets the same answer.
- **An unowned row is reachable by nobody.** If a row is written with no owner, it does not match the rule for any
  principal — it is not shared, it is stranded. Anonymous callers own nothing, so a feature that must work signed-out
  needs a different store.
- **You can grant MORE, never less.** A `partial entity` block of your own lands *alongside* the capability's rule
  and the grants combine, so you can add a support role that reads every conversation. You cannot use it to take the
  owner's access away.

⚠ **That combining rule is specific to this mechanism.** A row that is *part of* another row
([rows that are part of another row](https://osysharp.com/reference/security/part-of-derived-access/)) composes the other way: declaring a block there REPLACES the derived rule
rather than adding to it. The two differ because the questions do — a capability's own grant is a promise it makes
about its rows and yours cannot revoke it, while a derived rule is only a default standing in for a decision you had
not made yet.

```osy syntax
// Add to what the capability already grants — the owner's access stays.
partial entity ChatSession {
  security { allow read when user.IsSupportAgent; }
}
```

## Examples       {#examples}
An app that opts into conversation memory. There is no security block for `ChatSession` anywhere in it — the
capability brought its own, and the `[Principal]` is what it scopes to:

```osy title="opting in, with the one thing it requires" test app=security-capability-row-ownership
app Helpdesk { use Osysharp.Agents; }

using Osysharp.Agents;

// The capability needs a principal type to scope its rows to. Without this the compile is refused.
[Principal] entity User { string Email; }

entity Ticket {
  string Subject;
  security { allow read, create where Reporter == user; }
  User Reporter;
}
```

## See also       {#see-also}
- [Reading a capability's source](https://osysharp.com/reference/local/reading-a-capability/) — what a capability declares, and how to read its source
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block, and stating extra access with `partial entity`
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — why a table nobody granted is denied


---

<!-- https://osysharp.com/reference/security/navigation-predicates/ -->

# navigation in security predicates (any depth, either side)

> A `where` row filter may follow relations as far as the model goes — `Folder.Workspace.Region.Head == user` is a four-hop rule, and the principal side navigates too (`user.HomeRegion.Company`). There is no depth limit and no shape a shallow rule is allowed that a deep one is not: criteria on intermediate rows, several chains in one predicate, a chain inside a collection hop, negation and `== null` mid-chain all behave the same at any length. Read and write always agree, so a rule that grants you the read grants you the write.

<!-- id: security-navigation-predicates · area: security · stability: stable · html: https://osysharp.com/reference/security/navigation-predicates/ -->

## Summary        {#summary}
A `where` row filter is an ordinary query predicate, so it may **follow relations** to reach the fact that decides
access. The chain can be as long as your model:

```osy syntax
entity Note {
  [Required] Folder Folder;
  security {
    // four hops: this note's folder → its workspace → that workspace's region → the region's head.
    allow read, update where Folder.Workspace.Region.Head == user;
  }
}
```

**Nothing about depth changes the rules.** A four-hop rule may carry criteria on the rows it passes through, combine
several chains, sit inside a collection hop, or be negated — the same as a one-hop rule.

## Signature      {#signature}

```osy syntax
allow <verbs> where <chain> <op> <value>;         // <chain> is Rel.Rel….Property, any length
allow <verbs> where user.<chain> == <value>;      // the principal navigates too
```

## Description    {#description}

### Both sides navigate     {#both-sides}
The row under evaluation is reached with a bare property name, and the caller with `user`. Either may navigate:

```osy syntax
// the row's chain compared to the principal's chain
allow read where Folder.Workspace.Region.Company == user.HomeRegion.Company;
```

### A broken chain denies, it does not hide the row   {#null-chains}
If any relation along the chain is null, the comparison is **not true** — so that row satisfies no `allow` built on
the chain. It is *not* removed from consideration, which matters the moment the chain is one arm of something:

```osy syntax
// a workspace with no region is admitted by the LEFT arm; one with a region is decided by the right.
allow read where Folder.Workspace.Region == null || Folder.Workspace.Region.Head == user;
```

Both arms get their say for every row. A rule cannot accidentally exclude rows by mentioning a relation they lack.

### Read and write give the same answer     {#backend-parity}
Reads are answered by the database and writes are checked in memory at commit, but they evaluate **the same
predicate** and must return **the same verdict** — at every depth, on both sides of the comparison. You never have to
know which engine is deciding, and you never have to shorten a rule to keep them in agreement.

> **Do not denormalize a relation just to keep a rule short.** Copying a parent's key onto a child so the rule can
> say `Organization` instead of `Application.Organization` buys nothing here, and costs a column that can drift out
> of step with the relation it mirrors.

### Does a hop resolve against rows the caller cannot read?      {#unsecured-hops}
Following a relation resolves against the data **as it is**, not against the rows the caller may read. Deciding
authorization from what the caller can already see would be circular — you cannot read the membership row that grants
you the read. Only the final verdict reaches the caller; no row visited along the way is disclosed.

## Examples       {#examples}

```osy title="a four-hop rule, with criteria on the rows it passes through" test app=security-navigation-predicates
[Principal] entity User {
  string Email;
  security { allow read when IsAuthenticated; }
}

entity Region {
  string Name;
  [Required] User Head;
  security { allow read when IsAuthenticated; }
}

entity Workspace {
  string Name;
  Region Region;                                   // nullable — a workspace need not sit in a region
  [Required] bool Active;
  security { allow read when IsAuthenticated; }
}

enum FolderKind { Normal, Archived }

entity Folder {
  string Name;
  [Required] Workspace Workspace;
  [Required] FolderKind Kind;
  security { allow read when IsAuthenticated; }
}

entity Note {
  string Title;
  [Required] Folder Folder;
  security {
    // four hops to the deciding fact, plus criteria on TWO of the rows along the way.
    allow read, update where Folder.Workspace.Region.Head == user
                          && Folder.Workspace.Active
                          && Folder.Kind != FolderKind.Archived;
  }
}
```

```osy title="a chain inside a collection hop, correlated to the row" syntax
// a chain inside a collection hop, correlated to a chain on the row
allow read where RegionMember.Any(m => m.Region == Folder.Workspace.Region && m.User == user);
```

```osy title="the shape a denormalized FK used to exist for" syntax
// the shape a denormalized FK used to exist for — now written as it reads
allow update, delete where OrganizationMember.Any(o => o.Organization == Application.Organization
                                                    && o.User == user
                                                    && o.Role != OrgRole.Member);
```

## See also       {#see-also}
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block and its verbs
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — `IsAuthenticated` / `IsAnonymous`
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — why an unqualified `allow` is refused


---

<!-- https://osysharp.com/reference/security/naming-a-policy/ -->

# policy

> Names an authorization rule once so every place that needs it can say the name. A policy is a boolean about the caller, usable where security is declared and callable in ordinary code.

<!-- id: security-naming-a-policy · area: security · stability: stable · html: https://osysharp.com/reference/security/naming-a-policy/ -->

## Summary        {#summary}

A `policy` gives an authorization rule a name. Declare it once, then say the name wherever it applies — in an
entity's `security { }`, on a page, and inside a function as an ordinary boolean. A policy may take a parameter, which
is how you say *"may they manage **this** one"* rather than *"are they a manager of something"*.

## Signature      {#signature}

```osy syntax
policy Name => <predicate over `user`>;
policy Name(Type parameter) => <predicate over `user` and that parameter>;
```

## Description    {#description}

The predicate is about the **caller**. `user` is whoever is asking, and the rule may look at any data it needs to
decide — including rows the caller could not read themselves. That is deliberate: whether you *may* do a thing must
not depend on which rows you happen to be allowed to *see*, or every grant becomes circular.

```osy title="one rule, named" test app=security-naming-a-policy
[Principal] entity Person {
  [Required, MaxLength(200), Unique] string Email;
  security { allow read when IsAuthenticated; }
}

entity Team {
  [Required, MaxLength(80)] string Name;
  security { allow read when IsAuthenticated; }
}

entity TeamLead {
  [Required] Person Person;
  [Required] Team Team;
  security { allow read when IsAuthenticated; }
}

// "is a lead of some team" — a fact about the caller alone.
policy IsALead => TeamLead.Any(l => l.Person == user);
```

### Call it like the boolean it is   {#calling}

A policy reads as a boolean, so you can use it as one. This is the alternative to copying the predicate into every
function that needs it — one rule in as many copies as your app has entry points, each able to drift, in the one
category where drift is a hole rather than a bug.

```osy title="named, not re-written" test app=security-naming-a-policy
string LeadsOnly() {
  if (!IsALead) { throw new NotAuthorized("Only a team lead may do this."); }
  return "done";
}
```

### A parameter is what makes it about **this** one   {#parameters}

`IsALead` asks whether you lead *anything*. Most real authority is narrower — may you manage **this** team? Give the
policy a parameter and pass the row:

```osy title="authority about a particular row" test app=security-naming-a-policy
policy CanManageTeam(Team t) => TeamLead.Any(l => l.Person == user && l.Team == t);

string Rename(Guid teamId, string name) {
  var team = Team.Where(t => t.Id == teamId).FirstOrDefault();
  if (!CanManageTeam(team)) { throw new NotAuthorized("Only a lead of THIS team may rename it."); }
  team.Name = name;
  return name;
}
```

The difference matters more than it looks. A lead of *another* team passes `IsALead` and fails `CanManageTeam(team)` —
and that is usually the only case that tells a working rule from a broken one, because a manager and a non-member
behave the same under both.

### The rule the engine enforces can be composed too   {#in-a-where}

A `where` may CALL a parameterised policy, passing the row it is about. This is where naming rules stops being tidy
and starts being the difference between a rule you can read and one nobody dares touch.

Take a grant-handling system. Whether a caseworker may read a case is **four independent rules at once**:

- they work for the unit that owns the scheme it was applied under;
- they have not **recused** themselves from this particular case;
- if the case is **restricted**, they are senior enough to see one;
- and they are signed in at all.

Written inline, that is one welded expression — and it has to be repeated on every entity that hangs off a case:
the attachments, the assessments, the notes, the decision, the payment.

```osy title="the welded form — four rules, no names, copied five times" syntax
entity Case {
  security {
    allow read where RoleGrant.Any(g => g.Holder == user && g.Unit == Scheme.OwningUnit)
                  && !Recusal.Any(r => r.Person == user && r.Case.Id == Id && r.Lifted == false)
                  && (!Restricted || RoleGrant.Any(g => g.Holder == user && g.Level == Level.Senior));
  }
}
```

⚠ **The problem is not that it is long — it is that the fifth copy is where a clause goes missing**, and nothing
catches that. It compiles, that entity's own tests pass, and the defect is *"a caseworker read a case they had
recused themselves from"*. Nobody sees it until an audit.

Name each rule once, and the predicate reads like the sentence it enforces:

```osy title="the same rule, composed" test app=security-policy-in-a-where
[Role] enum Level { Caseworker, Senior }

[Principal] entity Person {
  [Required, MaxLength(200), Unique] string Email;
  security { allow read when IsAuthenticated; }
}
entity Unit { [Required, MaxLength(60)] string Name;
  security { allow read when IsAuthenticated; } }
entity RoleGrant { [Required] Person Holder; [Required] Level Level; Unit? Unit;
  security { allow read when IsAuthenticated; } }
entity Scheme { [Required, MaxLength(60)] string Name; [Required] Unit OwningUnit;
  security { allow read when IsAuthenticated; } }
entity Recusal { [Required] Person Person; [Required] Case Case; bool Lifted;
  security { allow read when IsAuthenticated; } }

// Each rule, named once. The first two are ABOUT a row, so they take one.
policy HandledByUnit(Unit u)   => RoleGrant.Any(g => g.Holder == user && g.Unit == u);
policy HasRecused(Guid caseId) => Recusal.Any(r => r.Person == user && r.Case.Id == caseId && r.Lifted == false);
policy MaySeeRestricted        => RoleGrant.Any(g => g.Holder == user && g.Level == Level.Senior);

entity Case {
  [Required, MaxLength(40)] string Reference;
  [Required] Scheme Scheme;
  bool Restricted;
  security {
    allow read where HandledByUnit(Scheme.OwningUnit)
                  && !HasRecused(Id)
                  && (!Restricted || MaySeeRestricted);
  }
}
```

**Nothing is given up for that.** A policy INLINES — the persisted predicate is byte-for-byte the welded one above,
so the database does the same work and a named rule costs nothing at run time. What changes is everything around it:

| | welded | composed |
|---|---|---|
| the recusal rule lives in | five places | one |
| renaming `Lifted` | five edits, and a miss compiles | one edit |
| a missing clause on entity five | silent | still silent — **but there is only one clause to miss** |
| reading the rule | parse the expression | read the names |

### And a query says nothing about it   {#in-a-query}

The rule lives on the entity, so a query over `Case` is just a query. It filters for what the caller is *looking
for*; who the caller is allowed to *see* is already inside the statement the compiler emits, composed from the four
rules above. Nothing here restates a grant, names a role, or checks anything:

```osy title="a query about cases, and not one word about who may see them" test app=security-policy-in-a-where
List<Case> RestrictedUnder(string scheme) {
  return Case.Where(c => c.Restricted && c.Scheme.Name == scheme)
             .OrderBy(c => c.Reference)
             .ToList();
}
```

A caseworker outside the unit gets an empty list, not an error; a recused one does not see the case they recused
from; a junior one sees no restricted case at all — and the function above is the same function for every one of
them. That is the point of putting the rule on the entity: there is no call site that could have forgotten it.

⚠ **Pass the ID, not the row.** `HasRecused(Id)` — a row predicate has no `this`, and the compiler will tell you so
(that is a fact about predicates, not about policies: the welded form cannot say `this` either).

### `when` or `where` — the argument decides   {#when-or-where}

A rule has two clauses: `when` asks who the **caller** is, once per request, with no row in hand; `where` narrows
**rows**, one at a time. A parameterised policy called with one of this entity's own members can only be about the
row — `IsAdmin(Organization)` written on `RoleGrant` means *this row's* `Organization`, and nothing else a C# reader
could take it for — so it is a row rule **whichever word you wrote**. Both of these compile, and they compile to the
same rule:

```osy title="organisation-scoped access, said either way" test app=security-policy-in-a-when
[Role] enum OrgRole { Admin, Member }

[Principal] entity User {
  [Required, MaxLength(200), Unique] string Email;
  security { allow read when IsAuthenticated; }
}
entity Organization { [Required, MaxLength(80)] string Name;
  security { allow read when IsAuthenticated; } }
entity Membership { [Required] User User; [Required] Organization Organization; [Required] OrgRole Role;
  security { allow read when IsAuthenticated; } }

// "is an admin of THIS organisation" — a fact about the caller AND a row.
policy IsAdmin(Organization o) => Membership.Any(m => m.User == user && m.Organization == o && m.Role == OrgRole.Admin);

entity RoleGrant {
  [Required] Organization Organization;
  [Required, MaxLength(40)] string Label;
  security {
    allow read when IsAdmin(Organization);                    // read: this row's Organization
    allow create, update, delete where IsAdmin(Organization); // the same rule, the other word
  }
}
```

The `create` line is a **with-check**: a new `RoleGrant` may only be written for an organisation the caller
administers, decided against the row being created. A `when` that mixes a caller fact with a row fact —
`when IsAuthenticated && IsAdmin(Organization)` — keeps the caller half as the `when` and makes the row half the
`where`. And `osy explain` reads either spelling as the one sentence it is: *"only rows of an Organization where the
caller's Membership has Role = Admin"*.

What the compiler still refuses, at the rule, naming what you passed and what would work:

- **A field-level rule's `when`** — `deny read Label when IsAdmin(Organization)` — cannot be about the row: a
  `when` guard runs once per request, before any row is in hand. Guard `when` on the caller alone, or write this
  rule's OWN `where` instead — a field mask may carry a row-scoped condition, exactly like a row rule's
  (`deny read Label where IsAdmin(Organization);`, [[security-entity-security#field-mask-row-scoped]]).
- **An entity that is not a member** — `IsAdmin(Organization)` on an entity with no `Organization` property. There
  the name IS the entity set (every `Organization`), which a policy taking one row cannot take; the refusal says so
  and offers both ways out: pass a specific row, or add the relation and write the row rule.

### What the compiler will not let you write   {#refusals}

- **Naming a parameterised policy without its argument** — `if (!CanManageTeam)` — is an error. There would be nothing
  for it to decide about, and a rule that quietly decides about nothing denies people who should be allowed.
- **Giving the wrong number of arguments** is an error, naming what the policy declares.
- **A lambda variable that shadows the policy's own parameter** — `CanManageTeam(Team t) => …Any(t => …)` — is an
  error. One name would mean two different rows.
- **Gating a page on a parameterised policy** — `[Authorize(CanManageTeam)]` — is an error. A page gate runs before any
  row is loaded, so there is no argument to give it. Gate the page on a plain policy and check the row inside.
- **Two policies that call each other** — `A(u) => B(u)` and `B(u) => A(u)` — is an error. There is no fixed
  predicate to inline, and a rule with no fixed meaning cannot be enforced.

## Examples       {#examples}

```osy title="the same policy, in all three places" test app=security-naming-a-policy
entity Memo {
  [Required] Team Team;
  [MaxLength(200)] string Note;
  security {
    allow read when IsAuthenticated;
    // Declared authority: the POLICY itself, passing the row it is about. This block used to spell the rule out
    // again by hand — the page claimed the policy was used in all three places while one of them was a copy.
    allow update, delete where CanManageTeam(Team);
  }
}

// …and named in code, for the authority the engine cannot carry for you — an elevated operation, an external call,
// anything where the decision is not a row read.
bool MayManage(Guid teamId) {
  var team = Team.Where(t => t.Id == teamId).FirstOrDefault();
  return CanManageTeam(team);
}
```

## See also       {#see-also}

[security { }](https://osysharp.com/reference/security/entity-security/) — declaring who may read and write an entity's rows.

[principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — `IsAuthenticated` and the other built-in facts about the caller.

[secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — why everything is denied until a rule grants it.


---

<!-- https://osysharp.com/reference/security/principal-predicates/ -->

# principal predicates (IsAuthenticated / IsAnonymous) and open reads

> Two built-in `when` predicates say who a request is: `IsAuthenticated` is a signed-in user, `IsAnonymous` is an unauthenticated visitor, and `IsAuthenticated || IsAnonymous` is everyone — said out loud. They are the sanctioned way to open a surface deliberately, because every `allow` must say WHO: a rule with no `when` and no `where` is a compile error, on all four verbs and in every app. An unqualified grant is not a weaker rule — it is the absence of one wearing the syntax of one.

<!-- id: security-principal-predicates · area: security · stability: stable · html: https://osysharp.com/reference/security/principal-predicates/ -->

## Summary        {#summary}
Two **built-in `when` predicates** describe the request's principal, for use in an entity `security { }` block:

- **`IsAuthenticated`** — a signed-in user is making the request.
- **`IsAnonymous`** — no one is signed in (the unauthenticated visitor).
- **`IsAuthenticated || IsAnonymous`** — everyone, stated explicitly.

They exist because a bare **unqualified `allow`** — any verb, with no `when` guard and no `where` row filter — is a
**compile error**. It hands that verb to every caller, anonymous included, and the runtime consults nothing: on a read
it is the shape by which a `[Principal]` leaks its own credential columns, and on a `delete` it is worse. So you must
**say who**: a role, a `where` filter, or one of these predicates.

```osy syntax
[Principal] entity User {
  string Email;
  security {
    allow read when IsAuthenticated;   // any signed-in user may read the directory (not the world)
  }
}
```

## Signature      {#signature}
```osy syntax
allow read when IsAuthenticated;                 // a signed-in user
allow read when IsAnonymous;                      // an unauthenticated visitor
allow read when IsAuthenticated || IsAnonymous;   // everyone, explicitly
allow read when IsAuthenticated || IsStaff;       // compose with your own policies
```

Usable in a `when` guard or a `policy` body (a per-request fact about the principal — not a `where` row filter), and
usable **whether or not the app declares a `[Principal]`**: an app with none has exactly one kind of caller, so
`IsAnonymous` is true there and `IsAuthenticated` is false. `IsAuthenticated` and `IsAnonymous` are **reserved** — a
`policy` may not take those names.

## Description    {#description}
The rule applies to **every app and all four verbs**, whether or not a `[Principal]` is declared. It once fired only
for `read`, and only on apps that had a principal; both exemptions are gone. The principal exemption in particular was
a trap rather than a kindness — a principal-less app was let off, and then silently inherited every unqualified grant
the day it declared a `[Principal]`, having never been asked. These two predicates are **always bound**, so an app with
no principal can still say exactly what it means (`when IsAnonymous`).

`deny` has its own rule rather than an exemption: an entity-level `deny` with no `when`/`where` is refused too — under deny-all it subtracts from nothing, or silently cancels the grant above it. A FIELD-scoped one (`deny read Secret;`) stays legal: property rules replace the entity's rather than layering on them, so it is the only way to say "nobody, ever". ⚠ A CREDENTIAL column is the one exception, and it is refused for a reason worth knowing: masking it from everyone masks it from your own sign-in too, so `Security.VerifyPassword` compares against null and a correct password is refused exactly like a wrong one. Condition that one on your auth policy — `deny read PasswordHash when !IsAuthenticator;`.

The predicates read as intent:

- **`IsAuthenticated`** grants the read to anyone signed in, regardless of role — the common "members can see the
  directory, the public cannot" case, without inventing a role for it.
- **`IsAnonymous`** grants it to the not-signed-in visitor — a genuinely public page or catalog. (It is the identity
  of the request, distinct from any role a user account might hold.)
- **`IsAuthenticated || IsAnonymous`** is the honest way to write "everyone": broad, but deliberate, and greppable —
  a reader sees that the openness was chosen, not forgotten.

Prefer the narrowest that is true. Reach for the `||` form only when a read really is public; reach for a role or a
`where Owner == user` filter when it is not.

## Examples       {#examples}
A `[Principal]` with a credential column, a members-only directory read, and a genuinely public catalog:

```osy title="a members-only directory and a genuinely public notice" test app=security-principal-predicates
[Role] enum AppRole { Authenticator }
entity RoleGrant { User User; [Required] AppRole Role; }

// The leash the auth flow is checked against — the credential is masked from everyone EXCEPT this.
policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);

[Principal] entity User {
  string Email;
  security {
    allow read when IsAuthenticated;                 // signed-in users see the directory; the world does not
  }
}

entity Announcement {
  string Body;
  security {
    allow read when IsAuthenticated || IsAnonymous;  // truly public — everyone, said out loud
  }
}
```

## See also       {#see-also}
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block these predicates live in
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — the deny-all posture that makes an unmentioned grant a denial
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — login/signup before a principal exists, run as a declared role
- [public pages (what a signed-out visitor can see and do)](https://osysharp.com/reference/security/public-reads/) — `IsAnonymous` in practice: what a signed-out visitor actually sees, and the half that is easy to forget


---

<!-- https://osysharp.com/reference/security/public-reads/ -->

# public pages (what a signed-out visitor can see and do)

> A public page, its public data and its public actions are three separate declarations. `[AllowAnonymous]` on a component says a signed-out visitor may see the PAGE; whether they get its ROWS is decided by the entity's own `security { }` block; whether they may RUN an action is a grant on the function it calls. Grant only the page and it renders perfectly with nothing in it, and its buttons do nothing — the quietest failures in a public app.

<!-- id: security-public-reads · area: security · stability: preview · html: https://osysharp.com/reference/security/public-reads/ -->

## Summary        {#summary}
A shop's catalogue, a published article, a price list: some data is meant for people who have not signed in. Making
that work takes **two declarations, on two different things**, because the page and its data are gated separately:

- **`[AllowAnonymous]` on the component** — a signed-out visitor may load the PAGE. Without it, routing sends them to
  your login route and they never see it.
- **an anonymous read grant on the ENTITY** — a signed-out visitor may read its ROWS. Without it, the page renders
  and every query on it comes back refused.

If the page also lets a visitor *do* something, there is a **third**: `[AllowAnonymous]` on the function the action
calls. See *Writing is a THIRD grant*, below.

```osy title="both halves, and the page works signed out" test app=security-public-reads
[Principal] entity User { [MaxLength(200)] string Email; }   // who "signed in" means, for the grants below

entity Product {
  [MaxLength(80)] string Name;
  decimal Price;
  security {
    allow read when IsAnonymous || IsAuthenticated;   // ← the DATA half: anyone may read the catalogue
    allow create, update, delete when IsAuthenticated;
  }
}

[Page("/")]
[AllowAnonymous]                                      // ← the PAGE half: anyone may load this route
component Catalog() {
  live var products = Product.ToList();
  render { foreach (var p in products) { Text(p.Name); } }
}
```

The two do not inherit from each other, in either direction. That is deliberate — a public page composed of private
data is a real design, and so is a private page over public data — but it means **granting one and forgetting the
other is a thing you can do**, and the failure is silent.

## Signature      {#signature}
```osy syntax
security {
  allow read when IsAnonymous;                    // signed-out visitors only
  allow read when IsAnonymous || IsAuthenticated; // everyone, said out loud
}
```

## Description    {#description}

### The half that is easy to forget   {#empty-page}
Under [deny-all](https://osysharp.com/reference/security/secure-by-default/), an entity you have not granted is denied to everyone — including the
anonymous visitor. So a page marked `[AllowAnonymous]` over an ungranted entity **renders**: the chrome, the headings,
the empty list, the `0 items` count. No error, no redirect, no console message. It simply shows nothing, and it looks
exactly like a catalogue with no products in it.

This is worth stating plainly because it survives the checks you would expect to catch it. It compiles. Its tests pass
— a test runs under a principal you choose, and the ordinary anonymous read those tests exercise is honoured correctly.
The page renders in every screenshot taken while signed in. **Only opening the app signed out shows the gap.**

The compiler does look for it. An `[AllowAnonymous]` page reading an entity with no anonymous grant raises the
`security-anon-page-reads-ungranted-entity` maturity finding, which names the page, the entity, and the line to add.

### What "reaches an anonymous caller" means   {#grants}
An entity is anonymously readable when its `security { }` block grants a read to a caller who is not signed in:

| Grant | Anonymous visitor |
|---|---|
| `allow read when IsAnonymous;` | reads it |
| `allow read when IsAuthenticated \|\| IsAnonymous;` | reads it |
| `allow read when IsAuthenticated;` | **refused** — an anonymous caller is not authenticated |
| `allow read where Owner == user;` | **refused** — there is no `user` to match |
| no `security { }` block at all | **refused** under deny-all |

There is no separate switch and no attribute to add: the grant IS the declaration. Say it once on the entity, and
every surface — the page's query, the boot bundle, the server-side render — follows it.

### Only the rows you granted   {#scope}
Granting an anonymous read opens exactly what the grant says and nothing beside it. The row filter still applies the
predicate per row, field-level `deny read` masks still apply, and an entity you did not grant stays invisible — not
merely unreadable, but absent from the model an anonymous session is served at all. So a public catalogue does not
drag the customer table into public view because they happen to be related.

Prefer the narrowest grant that is true. `IsAnonymous` alone is right when a page is public and the signed-in view is
a different one; the `||` form is right when the answer really is "everyone" — see
[principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/).

### Writing is a THIRD grant   {#writes}
Reading needs two declarations. A visitor who *does* something — fills a basket, casts a vote, submits a form —
needs three, because an action reaches a server function and **who may call a function is its own grant**:

| | says | if missing |
|---|---|---|
| `[AllowAnonymous]` on the component | a stranger may SEE the page | they are redirected to login |
| an anonymous read/write grant on the entity | a stranger's rows may be read/written | the read is empty; the write is refused at commit |
| `[AllowAnonymous]` on the function | a stranger may CALL it | the click posts and the server refuses it — a **dead button** |

```osy title="the write half" test app=security-public-reads-write
[Principal] entity User { [MaxLength(200)] string Email; }

entity Vote {
  [MaxLength(80)] string Choice;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }   // the ROWS
}

[AllowAnonymous]                                   // the FUNCTION — a stranger may run it
void CastVote() {
  var v = new Vote { Choice = "yes" };
  UnitOfWork.Commit();
}

[Page("/vote")]
[AllowAnonymous]                                   // the PAGE
component VotePage() {
  action Cast() { CastVote(); }
  render { Button("Vote", onPress: Cast); }
}
```

The third one is the quietest of the three: the page renders, the button is there, it looks enabled, and pressing it
produces nothing at all — the refusal never reaches the screen. The compiler flags it as
`security-anon-page-calls-gated-function`, naming the page, the action and the function.

An `[AuthMethod]` function needs no such mark: it is an anonymous entry point by construction. And an action that
signs someone in and then calls a gated function is fine — by that line the caller is no longer a stranger.

### Look at it signed out   {#verify}
The habit worth forming: open the app **in a private window**, on the real page, and check the data is there. A public
app's most common defect is not a refusal — a refusal is loud. It is a page that renders beautifully and is empty,
and the only reader who ever sees it is the one who is not signed in.

## Examples       {#examples}
A storefront: a public catalogue, a basket keyed to the browser, and a customer record that stays private.

```osy title="public catalogue, private customers" test app=security-public-reads-shop
[Principal] entity Customer {
  [MaxLength(200)] string Email;
  security {
    allow read where Id == user.Id;         // you read your own profile, and nobody reads anyone else's
    allow update where Id == user.Id;

  }
}

entity Product {
  [MaxLength(80)] string Name;
  decimal Price;
  security {
    allow read when IsAnonymous || IsAuthenticated;   // a catalogue is public — that is what a shop is
    allow create, update, delete when IsAuthenticated;
  }
}

[Page("/")]
[AllowAnonymous]
component Catalog() {
  live var products = Product.ToList();
  render {
    Stack(gap: 2) {
      Text(products.Count() + " items");
      foreach (var p in products) { Text(p.Name + " " + p.Price.ToString("C")); }
    }
  }
}
```

Signed out, this page shows the products. `Customer` is granted to nobody anonymous, so it is not readable and not
even described to an anonymous session — the public page cannot leak it by accident.

## See also       {#see-also}
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — `IsAnonymous` / `IsAuthenticated`, and why an open read must say so
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block the grant lives in
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — the deny-all posture that makes an unmentioned grant a denial
- [routes and pages](https://osysharp.com/reference/ui/routing/) — `[AllowAnonymous]`, and protected-by-default routing
- [Visitor](https://osysharp.com/reference/ui/visitor/) — `Visitor.Id`, for work a person begins before they are anybody


---

<!-- https://osysharp.com/reference/security/role-grants/ -->

# role grants (and the first admin)

> A role is granted by an ordinary entity — any entity that has both a reference to your `[Principal]` and a property typed as your `[Role]` enum is a role grant, recognised by its shape, with no marker to remember. Which means the security question is not "who may be an admin" but "who may CREATE a row in that table" — and that is a `security { }` block like any other. This page covers the shape, the union of several grant tables, the first-signup-becomes-admin bootstrap, and the rule that keeps it from becoming a self-elevation hole.

<!-- id: security-role-grants · area: security · stability: stable · html: https://osysharp.com/reference/security/role-grants/ -->

## Summary        {#summary}
A **role grant is an ordinary entity**. Any entity that has **both** a reference to your `[Principal]` **and** a
property typed as your `[Role]` enum *is* a grant table — recognised by that shape, with no marker to remember and no
special grammar:

```osy title="this is a role grant, because of its shape" test app=security-role-grants
[Role] enum AppRole { Authenticator, Member, Admin }

[Principal]
entity User {
  [Required, Unique, MaxLength(255)] string Email;
  [MaxLength(200)] string? PasswordHash;
  security {
    allow read   when IsAuthenticator;
    allow create when IsAuthenticator;
    allow read where Id == user.Id;
    deny read PasswordHash when !IsAuthenticator;
  }
}

entity RoleGrant {
  [Required] User User;                       // ← a [Principal] reference
  [Required] AppRole Role = AppRole.Member;   // ← a [Role] enum property   ⇒ this table grants roles
  security {
    allow read where User == user;            // you may see your own role
    allow read when IsAdmin;
    allow create, update, delete when IsAdmin;   // ONLY an existing admin hands out roles…
    allow create when IsAuthenticator;           // …and the sign-up flow, for the first-admin bootstrap
  }
}

policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);
policy IsAdmin         => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Admin);
```

Everything else follows from that. The question "who may be an admin?" **is** the question "who may create a row in
`RoleGrant`?" — and that is answered by a `security { }` block, in the same language as the rest of your app.

## Signature      {#signature}
```osy syntax
[Role] enum <RoleEnum> { <Member>, … }        // the app's ONE role vocabulary

entity <AnyName> {                             // the shape, not the name, is what makes it a grant
  <PrincipalEntity> <ref>;                     //   a reference to the [Principal]
  <RoleEnum> <prop>;                           //   a property typed as the [Role] enum
  security { … }                               //   ← who may write a grant. This is the security decision.
}

policy <Name> => <Grant>.Any(g => g.<ref> == user && g.<prop> == <RoleEnum>.<Member>);
```

## Description    {#description}

### How does the engine find my grant table?   {#by-shape}
There is no `[RoleGrant]` attribute, because there does not need to be: an entity that references the principal and
carries the role enum can be nothing else. Name it `RoleGrant`, `Membership`, `PlatformRoleGrant` — the engine finds
it by its shape and reads a user's roles from it.

**The user reference must be the GRANTEE** — the person the row is about. A reference named `User`, `Grantee`,
`Member`, `Principal`, `Subject`, `Account`, `Holder` (or after your principal entity itself) is one; so is the
entity's only user reference, unless its name says it is an *actor* — a `…By` suffix (`InvitedBy`, `CreatedBy`,
`ApprovedBy`) or an agent noun (`Inviter`, `Creator`, `Author`, `Approver`, `Reviewer`, `Requester`, `Sender`,
`Granter`, `Assigner`, `Issuer`, `Reporter`, `Actor`). So `Invitation { Organization Org; string Email; OrgRole
Role; User InvitedBy; }` is **not** a grant table: it carries a user reference beside a role, but the reference is
the inviter, and an invitation grants nobody anything. The linter, the role resolver and `osy user add` share this
one rule, so they can never disagree about which tables grant.

**An app may have several grant tables, and a user's roles are the union of all of them.** A global `RoleGrant` plus a
project-scoped `ProjectMember` is an ordinary thing to want, and it works without ceremony.

**A property typed as an ordinary enum is not a role — it is data, and that is usually what you want.**

```osy title="a plain enum on an entity is membership data, not a grant" syntax
enum OrgRole { Owner, Admin, Member }              // a plain domain enum — NOT the app's [Role] enum

entity OrgMember { User User; Organization Org; OrgRole Role; }   // not a role GRANT — it is membership data
```

`OrgMember` is a perfectly good entity and `Owner`/`Admin` are perfectly good values. They simply live in your data
rather than in the caller's ticket — and a scoped tier (*admin **of Acme***) has to live there, because a role name
carries no scope. You use them exactly as you would any other data, in a `where` filter:

```osy title="scoped membership belongs in a where filter" syntax
allow update where OrgMember.Any(m => m.Org == Org && m.User == user && m.Role != OrgRole.Member);
```

You may **also** use such a membership check as a `when` guard or an [[ui-authorize|`[Authorize]`]] policy — it is
answered by looking for the row:

```osy title="the same check as a policy — about the person, not this org" syntax
policy IsOrgAdmin => OrgMember.Any(m => m.User == user && m.Role != OrgRole.Member);   // "admin of ≥1 org"
```

But be precise about what such a policy can mean. A `when` guard and `[Authorize]` are asked **before any row is in
hand**, so they can only answer questions about *the person* — "is this user an admin of **some** org" — never "…of
**this** org". The per-org half is a question about a row, and it belongs in a `where`. Gate the page coarsely; scope
the data by row. (The full split is in [security { }](https://osysharp.com/reference/security/entity-security/).)

### An app has exactly ONE `[Role]` enum   {#one-role-enum}
It is tempting to mark `OrgRole` as `[Role]` too, so that org-admins get a "real" role. **You cannot: a second
`[Role]` enum is a compile error.**

```osy syntax
[Role] enum PlatformRole { Authenticator, User, Admin }
[Role] enum OrgRole      { Owner, Admin, Member }
//     ^^^^ an application may declare only ONE `[Role]` enum — `PlatformRole` is already the app's role
//          vocabulary, so `[Role]` here grants nothing. A second tier of membership is ordinary data:
//          drop the attribute and write a policy over the membership entity.
```

The rule is worth understanding rather than just obeying, because it tells you what a role **is**:

**`[Role]` is the vocabulary the login ticket carries.** One enum, resolved once, naming what the *person* is —
platform-wide, unscoped. That is why a second one cannot simply be added alongside it: a principal's roles are a
**flat list of names**, so merging two vocabularies would collapse `OrgRole.Admin` and `PlatformRole.Admin` into the
same name — and `IsPlatformAdmin`, which asks whether the caller holds `Admin`, would answer **yes** to the admin of
any throwaway org. Anyone could make themselves a platform admin by creating an organisation. The compiler refuses
the second enum so that nobody is ever tempted to "fix" it that way.

**And you do not need one.** A scoped tier could not be a role anyway — a role name carries no scope, so "admin **of
Acme**" is unsayable in a flat list, and only a row can hold it. Membership *is* the natural home for that, and a
policy over it is a first-class rule: it works in a `security { }` block and in `[Authorize(…)]` alike, exactly as
shown above. Nothing is lost by keeping `OrgRole` a plain enum — the tier is more expressive as data than it ever
could have been as a role.

### The security decision is on the grant table   {#the-decision}
If any signed-in user could create a `RoleGrant` row naming themselves and `Admin`, then every user is an admin, and
every other rule in your app is decoration. So the grant table's `security { }` block is the most consequential one
you will write. The shape that works:

```osy syntax
security {
  allow read where User == user;               // see your own role
  allow read when IsAdmin;                     // an admin sees who holds what
  allow create, update, delete when IsAdmin;   // only an existing admin grants a role
}
```

Note what is **absent**, and how deliberately: there is no `allow create where User == user`. That line would read
innocently — "a user may create their own grant" — and it would let anyone make themselves an admin. **A grant is
never self-written.** The authority to hand out a role comes from *already having* the authority, which is what stops
the ladder from being climbable from the ground.

And note that `update` and `delete` are listed explicitly. A rule that guards `create` and forgets `update` lets a
`Member` grant be *edited* into an `Admin` one — the same hole through a different verb. Grant all three to the admin,
or none.

`osy lint` holds this line for you: `security-grant-write-unguarded` is a MUST on any write to a grant table that
**the person the grant names can perform by being named** — `allow update where User == user`, a bare `allow create`,
a `where` that never mentions `user` at all (`where Role != Role.Admin` narrows which *rows*, never which *callers*).
It judges what the guard **says**, not which keyword it uses — which matters the moment a grant carries a scope.

### Who may write a grant that belongs to an organisation?   {#tenant-scoped}
In a multi-organisation app the grant table carries the tenant it applies to, and "an admin" means *an admin of
**this** row's organisation* — not an admin somewhere. That is a question about the row, so it cannot be a `when`
(a `when` is asked before any row is in hand, and has no `Organization` to read). It **has** to be a `where`, and a
`where` here is exactly as sound as the `when` above, because the person the grant names cannot satisfy it by being
named — holding an admin membership is a fact about a *different* table:

```osy title="an org-scoped grant table, guarded by the caller's membership in THIS row's organisation" test app=security-role-grants-tenant
[Role] enum Role { Authenticator, Member, Admin }
enum OrgRole { Admin, Member }                     // the tenant tier — plain data, not the app's [Role]

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

entity Organization {
  [Required("Give the organisation a name."), MaxLength(100)] string Name;
  security { allow read when IsAuthenticated; }
}

entity Membership {                                 // who holds which tier, in which organisation
  [Required("Pick the person.")] User User;
  [Required("Pick the organisation.")] Organization Organization;
  [Required("Pick a tier.")] OrgRole Role;
  security { allow read when IsAuthenticated; }
}

// "The caller holds Admin in THIS organisation" — a rule ABOUT a row, so it takes one.
policy IsAdmin(Organization o) => Membership.Any(m => m.User == user && m.Organization == o && m.Role == OrgRole.Admin);
policy IsAuthenticator        => RoleGrant.Any(g => g.User == user && g.Role == Role.Authenticator);

entity RoleGrant {
  [Required("Pick the organisation.")] Organization Organization;   // ← the scope
  [Required("Pick the person.")] User User;                          // ← a [Principal] reference
  [Required("Pick a role.")] Role Role;                              // ← a [Role] enum property ⇒ a grant table
  security {
    allow read where User == user;                       // see your own roles
    allow read, create, update, delete where IsAdmin(Organization);   // an admin OF THIS ROW'S organisation
    allow create when IsAuthenticator;                   // the sign-up flow, for the first-admin bootstrap
  }
}
```

The check may also sit inline — `allow create, update, delete where Membership.Any(m => m.User == user &&
m.Organization == Organization && m.Role == OrgRole.Admin);` reads the same and lints the same; the named form is
just the one you can reuse on `Invitation`, `Budget` and everything else that hangs off an organisation
([policy](https://osysharp.com/reference/security/naming-a-policy/)).

What the linter reads, in either spelling: **can the row's own subject get through this predicate while holding no
grant, membership or ownership anywhere?** `User == user` — yes, by definition, so it is refused. `Membership.Any(m
=> m.User == user && …)` — no, so it is sound. And `User == user || IsAdmin(Organization)` is refused again: the `||`
lets the subject through whatever stands beside it.

### How does the FIRST admin of a FRESH tenant get seated?   {#the-founding-shape}
`IsAdmin(Organization)` above has a hole in it that only shows up once: a brand-new organisation has no Membership
and no RoleGrant row yet, so `IsAdmin(Organization)` is FALSE for **everyone** — including the person who just
created it. Nobody could ever grant themselves (or anyone) the first role in an organisation they just founded.

The natural rule — "you may grant yourself Admin, but only if this organisation has no grant yet" — is SOUND, and
it is sound for a reason the compiler enforces rather than one you have to trust:

```osy title="founding an organisation makes you its admin" test app=security-role-grants-founding
[Role] enum Role { Authenticator, Member, Admin }

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

entity Organization {
  [Required("Give the organisation a name."), MaxLength(100)] string Name;
  security {
    allow read when IsAuthenticated;
    allow create when IsAuthenticated;   // anyone may found one — RoleGrant's own guard is what makes that safe
  }
}

entity RoleGrant {
  [Required("Pick the organisation.")] Organization Organization;
  [Required("Pick the person.")] User User;
  [Required("Pick a role.")] Role Role;
  security {
    allow read where User == user;
    allow read, create, update, delete where RoleGrant.Any(g => g.User == user && g.Organization == Organization && g.Role == Role.Admin);
    // THE FOUNDING SHAPE: you may grant YOURSELF Admin, but only where this organisation holds NO grant yet.
    allow create where User == user && Role == Role.Admin && !RoleGrant.Any(g => g.Organization == Organization);
  }
}

policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == Role.Authenticator);
```

Three things make this the ONE self-write the linter's `security-grant-write-unguarded` MUST accepts, and it
accepts nothing looser:
1. **The caller names themselves** — `User == user` — because a founding grant can only ever be for the founder.
2. **The role is ONE specific, literal member** — `Role == Role.Admin` — never "whatever role the caller asks for".
3. **The existence check is SELF-referential and scoped to nothing but the row's own tenant column** —
   `!RoleGrant.Any(g => g.Organization == Organization)`, not `g.User == user` (that checks the CALLER's history,
   which lets a fresh attacker re-target an organisation someone else already founded) and not a filter on some
   other field (`g.Role == Role.Member`, say — that blocks re-entry only once THAT filter's condition holds, not
   once any admin grant does).

That third point is what makes this SOUND rather than merely plausible: a CREATE's own guard is
evaluated with the row being inserted **excluded** from its own `.Any(…)` — so the very first `RoleGrant` for an
organisation sees no rows and passes, and the moment it commits, every later attempt against the SAME organisation
sees that row and is refused. The guard can pass for a given organisation **at most once**, no matter who tries or
how many times:

```osy title="the first founder becomes admin; a second founder in the same organisation is refused" run app=security-role-grants-founding
[TestFixture]
void OneFoundedOrgAndTwoUsers() {
  var acme  = new Organization { Name = "Acme" };
  var alice = new User { Email = "alice@example.com" };
  var bob   = new User { Email = "bob@example.com" };
  new RoleGrant { Organization = acme, User = alice, Role = Role.Admin };   // Acme is already founded
}

principal Bob => User.Single(u => u.Email == "bob@example.com");

[Test(OneFoundedOrgAndTwoUsers)]
[runas(Bob)]
void a_fresh_organisation_can_be_founded_by_its_first_admin() {
  var fresh = new Organization { Name = "Fresh Co" };
  var me = User.Single(u => u.Email == "bob@example.com");
  var grant = new RoleGrant { Organization = fresh, User = me, Role = Role.Admin };
  Assert.Equal(Role.Admin, grant.Role);
}

[Test(OneFoundedOrgAndTwoUsers)]
[runas(Bob)]
void a_second_founder_in_the_same_organisation_is_refused() {
  var acme = Organization.Single(o => o.Name == "Acme");
  var me = User.Single(u => u.Email == "bob@example.com");
  Assert.Denied(() => new RoleGrant { Organization = acme, User = me, Role = Role.Admin });
}
```

If you would rather nobody self-founds at all — every organisation is created BY an operator, for a customer —
see [[#platform-superadmin]] below.

### Can an OPERATOR create tenants, without any self-service rule at all?   {#platform-superadmin}
The other sanctioned shape has no privilege-escalation reasoning to check, because nothing in it is
self-written: a platform-level role, held by whoever runs the platform, gates tenant creation directly. Seed it
with `osy user add <email> --role SuperAdmin` — an OPERATOR verb, run once against a fresh instance, never a row
your app's own code writes.

```osy title="an operator-only role that creates tenants — no self-service, no founding rule to reason about" test app=security-role-grants-superadmin
[Role] enum PlatformRole { Authenticator, SuperAdmin, Member }

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

entity RoleGrant {
  [Required] User User;
  [Required] PlatformRole Level = PlatformRole.Member;
  security {
    allow read where User == user;
    allow read when IsSuperAdmin;
    allow create, update, delete when IsSuperAdmin;   // only an existing SuperAdmin hands out roles…
    allow create when IsAuthenticator;                // …and the sign-up flow, for the first-admin bootstrap
  }
}

entity Organization {
  [Required("Give the organisation a name."), MaxLength(100)] string Name;
  security {
    allow read when IsAuthenticated;
    allow create when IsSuperAdmin;   // only the operator (or a SuperAdmin they seated) may found a tenant
  }
}

policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Level == PlatformRole.Authenticator);
policy IsSuperAdmin    => RoleGrant.Any(g => g.User == user && g.Level == PlatformRole.SuperAdmin);
```

`RoleGrant` here carries no `Organization` at all — `PlatformRole` is a GLOBAL vocabulary, exactly like the
app-wide `[Role]` enum every app already has ([[#one-role-enum]]); a `SuperAdmin` is not an admin of any one
tenant, they are the operator. `allow create when IsSuperAdmin;` on `Organization` needs no `where`, no existence
check and no founding reasoning, because the caller is never the row's own subject — the whole self-elevation
question this page spends most of its words on simply does not arise. The trade is product, not security: nobody
signs themselves up for a tenant; an operator (or a SuperAdmin they seated) creates one for them.

### The bootstrap problem: where does the FIRST admin come from?   {#first-admin}
Only an admin may grant `Admin`. On an empty database there is no admin — so nobody can ever become one. Something
must break the circle, and it must break it **exactly once**.

The signup flow is the natural place, because the very first account is the only moment the answer is unambiguous:

```osy title="the first account bootstraps the admin; every later one does not" test app=security-role-grants
[AuthMethod]
string Signup(string email, string password) {
  bool isFirst = User.Count() == 0;             // ← evaluated BEFORE the create, or it is never true

  var u = new User { Email = email, PasswordHash = Security.HashPassword(password) };
  if (isFirst) {
    var grant = new RoleGrant { User = u, Role = AppRole.Admin };
  }
  return Security.IssueJwt(u.Id, u.Email);
}

[AuthMethod]
string Login(string email, string password) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  // Spend the same time either way — an early return makes "no such account" measurably faster
  // than "wrong password", and that difference is an enumeration oracle anyone can time. The
  // one-argument form verifies against nothing, costs a full KDF, and answers false.
  if (u == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); }
  return "";
}

app.AuthBootstrap = new AuthBootstrap { Role = AppRole.Authenticator, Login = Login, Signup = Signup };
```

Three details in that function are load-bearing, and each of them is a bug if you get it wrong:

1. **`isFirst` is captured *before* the create.** Read it afterwards and the count is 1, never 0 — the app would have
   no admin, ever, and the failure would look like a permissions problem rather than an ordering one.
2. **`User.Count()` is honest.** Like every query, it counts only the rows the caller may **read** — so this works
   *because* the auth role was granted a read on `User`. An auth role that could not read `User` would count 0 every
   time, and every signup would mint an admin. The grant is not a formality; it is what makes the count mean what you
   think it means.
3. **The `allow create when IsAuthenticator` on `RoleGrant` is what permits the grant** — and it is the *only* reason
   this line is allowed to work. The signup flow runs as the [ephemeral principal](https://osysharp.com/reference/security/auth-bootstrap/), so it
   writes under the auth role's grants like anything else.

### Why this is not a self-elevation hole   {#no-self-elevation}
`allow create when IsAuthenticator` looks alarming at first — the sign-up path may mint a role grant! Read what
actually holds it in place:

- **The auth role is not something a user can be.** It is an [ephemeral identity](https://osysharp.com/reference/security/auth-bootstrap/) the engine
  mints, with no user behind it, and **only** for a caller with no ticket, and **only** while running one of the
  functions you wired into `app.AuthBootstrap`. A signed-in user cannot acquire it — invoking `Login` while
  authenticated does not elevate them.
- **So the reachable surface of that grant is exactly the body of `Signup`** — a function you wrote, that you can read
  on one screen, and that mints `Admin` only when the user table is empty.
- **There is no other path.** No ordinary user, and no org-admin managing their own members, can write a
  `RoleGrant` — because no rule grants them `create`. The authority to make an admin is held by exactly two things:
  an existing admin, and a one-shot bootstrap that stops working the moment it succeeds.

That is the invariant worth keeping as you extend the app: **a lower tier must never be able to grant a higher one.**
When you add an org-membership table, an app-role table, an invite flow, ask the question again each time — the hole is
never the rule you wrote, it is the verb you forgot.

### The test that proves a member cannot self-elevate   {#prove-it}
This is a rule you should not merely believe. `runas` lets a test assert the denial directly — that an ordinary member
cannot make themselves an admin.

**Read the seed first: it is under a `runas` too, and it has to be.** A `[Test]` body outside a `runas` block is an
**anonymous caller** — not an exempt authoring context — so `new User { … }` there is subject to
`allow create when IsAuthenticator` exactly like the same line in a function, and is refused. The identity that may
mint a `User` and their first grant is the one the sign-up flow itself runs as, and
[`runas (AuthBootstrap)`](https://osysharp.com/reference/testing/runas/) is how a test stands there. (The other place a seed may be written
unsecured is a [[testing-test|`[TestFixture]`]], which runs unrestricted by design — but *only* the fixture; the rule
above is what holds inside a `[Test]`.)

```osy title="the test that proves a member cannot self-elevate" run app=security-role-grants
[Test]
void A_member_cannot_grant_themselves_admin() {
  // The seed is a WRITE, and a [Test] body is NOBODY — so these two creates obey `allow create when
  // IsAuthenticator` just as they would in the app. AuthBootstrap is the ephemeral principal the sign-up flow
  // runs as, and in this app it is the only identity that may mint a User and their first grant.
  User? member = null;
  runas (AuthBootstrap) {
    member = new User { Email = "m@example.com" };
    var membership = new RoleGrant { User = member, Role = AppRole.Member };
  }

  runas (member) {
    Assert.Denied(() => new RoleGrant { User = member, Role = AppRole.Admin });
  }
}
```

## See also       {#see-also}
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — the ephemeral principal the signup bootstrap runs as, and what leashes it
- [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/) — the `[AuthMethod]` marker on `Login` / `Signup`
- [security { }](https://osysharp.com/reference/security/entity-security/) — `when` (the person) vs `where` (the row), and the four verbs
- [The security model](https://osysharp.com/reference/security/index/) — the security model end to end
- [runas](https://osysharp.com/reference/testing/runas/) — proving a rule denies the person it should


---

<!-- https://osysharp.com/reference/security/part-of-derived-access/ -->

# rows that are part of another row

> 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.

<!-- id: security-part-of-derived-access · area: security · stability: preview · html: https://osysharp.com/reference/security/part-of-derived-access/ -->

## Summary        {#summary}
A platform shape often decomposes into more than one table. A [schedule](https://osysharp.com/reference/scheduling/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      {#signature}
```osy syntax
// 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    {#description}

### Why the count of tables is not a count of decisions   {#why}
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    {#transitive}
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   {#row-filter}
The parent's `where` clause is not dropped on the way down — it is **re-rooted**, so it keeps meaning the same thing:

```osy syntax
// 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   {#override}
A child that declares its own `partial entity … { security { } }` uses that and derives **nothing**:

```osy syntax
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](https://osysharp.com/reference/security/capability-row-ownership/) 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   {#which-tables}
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   {#copies-vs-hops}

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`.

```osy syntax
// 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   {#hop-derives}

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`](https://osysharp.com/reference/types/markdown/) field, whose sections are exactly this kind of child.

```osy title="an org-scoped Markdown field, secured by a hop-based tenancy rule" test app=security-part-of-derived-hop
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       {#examples}
A nightly digest, in full. One security decision, and everything the schedule decomposes into follows it:

```osy title="one decision for the whole shape" test app=security-part-of-derived
[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       {#see-also}
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — the deny-all posture these rows are an exception to, and why
- [capability rows that belong to a user](https://osysharp.com/reference/security/capability-row-ownership/) — capability rows owned by a user, whose blocks COMBINE rather than replace
- [Schedule (recurring work)](https://osysharp.com/reference/scheduling/schedule/) — the shape this is most often met through


---

<!-- https://osysharp.com/reference/security/secure-by-default/ -->

# 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 user request, and you grant access explicitly where you want it. There is no setting, no opt-out and no app that runs open — the one thing that genuinely has to run before anyone is signed in is declared as an auth-bootstrap. System, designer, and bootstrap contexts are never affected.

<!-- id: security-secure-by-default · area: security · stability: stable · html: https://osysharp.com/reference/security/secure-by-default/ -->

## Summary        {#summary}
An entity with **no `security { }` block** is **denied to every user request** — access is granted only where you
write it. This is the *secure-by-default* posture: you cannot forget to lock down a table, because an unmentioned
table is already locked. It is not a setting you turn on and there is no way to turn it off, so there is nothing to
write and nothing to remember: every app is deny-all, every entity starts closed, and the only question left is
which access you grant back.

```osy syntax
// Deny-all is the default — no security block → denied to every user request.
entity Ledger { string Entry; }

// Grant access back explicitly — anyone may read the catalog.
entity Product {
  string Name;
  security { allow read when IsAuthenticated || IsAnonymous; }
}
```

## Signature      {#signature}
```osy syntax
entity Ledger { string Entry; }                              // no block  → denied to every user request
security { allow read when IsAuthenticated; }                // a block   → a LIST OF GRANTS, and the only way in
```

There is nothing app-level to write. The posture is a property of the language, not a configuration value, so it
cannot be relaxed for an app, a file or an entity — the only lever is which grants you write in a `security { }`
block. The one thing that genuinely has to run before anyone is signed in — login, signup, a password reset — is
declared as an [auth-bootstrap](https://osysharp.com/reference/security/auth-bootstrap/) and runs under an identity you defined, on a leash of the
grants you gave that role.

## Description    {#description}
The posture changes only what a **user request** may touch. It does **not** change how the platform runs itself:

- **User requests** (a logged-in or anonymous caller reading/writing app data) are subject to the posture. An entity
  with no `security { }` block is denied; an entity **with** a block is governed by exactly what that block grants.
- **System, designer, and bootstrap** work (schema evolution, seeding, the metadata designer) is **never** affected —
  it does not run through a user's security context, so the posture never denies the platform's own operations.
- **Platform (`osy.*`) entities are exempt** — their access is gated by other layers, so the posture never denies a
  built-in read.

Grant access back with ordinary [security { }](https://osysharp.com/reference/security/entity-security/) rules — `allow read where Owner == user;`, a role check,
or one of the built-in [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) (`allow read when IsAuthenticated`, `when IsAnonymous`, or
`when IsAuthenticated || IsAnonymous` for a genuinely public read).

- **Every `allow` must say WHO — a rule with no `when` and no `where` is a compile error.** Deny-all decides what
  happens when you say *nothing*; this decides what happens when you open a block and then forget to qualify a line
  inside it. An unqualified `allow` grants that verb to every caller, anonymous included, and the runtime consults
  nothing — so it is not a weaker rule, it is the **absence** of one wearing the syntax of one, and it is worse than
  no block at all because deny-all would have caught the omission. It applies to all four verbs and to every app,
  whether or not it declares a `[Principal]`.

  The remedy is never to remove the grant — it is to name who holds it:

  | you mean | you write |
  |---|---|
  | the owner of the row | `allow update where Owner == user;` |
  | anyone signed in | `allow read when IsAuthenticated;` |
  | signed-out visitors (signup, a public form) | `allow create when IsAnonymous;` |
  | genuinely everyone | `allow read when IsAuthenticated \|\| IsAnonymous;` |

  That last row is supported and is the right answer for a store front — **"everyone" is a legitimate decision, and
  spelling it out is the whole point.** What is refused is leaving it unsaid. `when true` is a compile error for the
  same reason: it says *yes* without saying *who*, so a reader cannot tell a public surface from an unfinished one.
  The diagnostic names the form that does mean everyone.

- **An entity-level `deny` must say WHEN as well.** "Denies more, never less" is the reasoning of an
  allow-by-default system; under deny-all there is nothing broader to narrow. A bare `deny update;` on an entity is
  either dead text — the verb was already denied — or, when a sibling `allow` names the same verb, a silent kill
  switch: an unconditional deny is checked FIRST, so it cancels the grant written above it. Both readings are worse
  than deleting the line, which is the same argument that refuses `default deny;`.

- **A FIELD-scoped `deny` needs no condition — except on a CREDENTIAL.** `deny read PasswordHash when !IsAuthenticator;` subtracts one column
  from whatever the entity grants, because property rules REPLACE the entity's rather than layering on them — it is
  the only way to say "nobody, ever", and no `when` states it as cleanly.

- **A `partial entity` is exempt too**, since narrowing a declaration made elsewhere is the whole reason it
  exists.

- **There is no way out, and that is the point.** Deny-all holds for every app, always — so "we will turn security on
  before we ship" is not a state this platform can be in, and an app that appears to work is an app whose grants you
  actually wrote. The only sanctioned path past a locked table is the one that must exist: login and signup have to
  reach a user row before a principal exists, so you declare them in `app.AuthBootstrap` and the engine runs them as
  an ephemeral principal bearing a role you chose — which may touch exactly what your `security { }` blocks grant
  that role, and nothing else. See [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/), and [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) for page-level authorization,
  the request-time complement to entity-level deny-all.

## Examples       {#examples}
A public catalog an anonymous visitor may browse, alongside a private table only an owner sees — under the posture,
the untouched `Ledger` is denied to all:

```osy title="granted, filtered, and denied — in one app" test app=security-secure-by-default
[Principal] entity User { string Name; }

entity Product {                                   // anonymous visitors may browse the catalog
  string Name;
  security { allow read when IsAnonymous || IsAuthenticated; }
}

entity Order {                                     // a buyer sees only their own orders
  User Buyer;
  string Item;
  security { allow read where Buyer == user; }
}

entity Ledger { string Entry; }                    // no security block → denied to everyone (the default)
```

## See also       {#see-also}
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block that grants access back
- [rows that are part of another row](https://osysharp.com/reference/security/part-of-derived-access/) — the exception: a row that is PART OF another takes that row's rule, with no block of its own
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — `IsAuthenticated`/`IsAnonymous`, and why every `allow` must say who
- [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) — page-level authorization, the request-time complement to deny-all


---

<!-- https://osysharp.com/reference/security/entity-security/ -->

# 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 own); a when clause gates by the principal (staff see everything). Rules are compiled into every query.

<!-- id: security-entity-security · area: security · stability: stable · html: https://osysharp.com/reference/security/entity-security/ -->

## Summary        {#summary}
A `security { }` block on an entity decides who may read and write its rows. It is not a check you call — it is
**compiled into every query and every write**, so there is no code path that can go around it. A row you may not see
is not fetched and hidden; it is never in the result at all.

Two kinds of rule, and the distinction is the whole model:
- **`where`** filters by the **row** — *the owner sees their own rows*.
- **`when`** gates by the **principal** — *staff see everything*.

They differ in **what they ask**, never in **when they ask it**. Both are decided against the facts as they stand at
the moment of the read or the write — including a grant your own code committed a line earlier. See
[[#same-freshness|the same freshness]].

A `where` on an **`update`** is asked about the row **twice** — as it is, and as it will be — and both must pass. That
is what makes a status gate a gate: see [[#update-both-images|before and after]].

A rule may also name a single **field**, and that means one thing on reads and a different thing on writes: on `read`
it edits a [[#field-mask|mask]] (which columns are hidden), on a write verb it [[#field-write|replaces]] the entity's
rule for that column. A read mask may take **either** a `when` (principal-only) or a
[[#field-mask-row-scoped|`where`]] (row-scoped, decided per row) — the same two shapes an entity-level rule takes.

## Signature      {#signature}
```osy syntax
entity <E> {
  // Everything is denied already. You do NOT write `default deny` — you only write what is ALLOWED.
  security {
    message "…";                         // what a person reads when a WRITE here is refused (optional)

    allow <verbs> when  <policy>;        // …to principals satisfying this policy
    allow <verbs> where <row predicate>; // …only the rows matching this

    // A rule may name ONE FIELD, and read and write mean different things by that:
    deny  read <Field> when <policy>;    // a MASK, principal-only — the row comes back, the column does not
    deny  read <Field> where <row predicate>;  // a MASK, row-scoped — decided per row, like an entity rule's `where`
    allow read <Field> when <policy>;    // LIFTS that mask again, for these callers
    allow <write verb> <Field> when <policy>;  // REPLACES the entity's rule for this column
  }
}

// <verbs> — one, or several, comma-separated:
//   read · create · update · delete

policy <Name> => <predicate over `user`>;   // a named, reusable principal test
```

## Description    {#description}

### You only ever write what is ALLOWED   {#only-allow}
Everything is denied before you say a word, so a `security { }` block is a list of **grants**. There is nothing to
turn off first, and **you never write `default deny`** — that is the state you are already in.

The clearest demonstration is the locked entity: to make one that nobody may read or create, say **nothing at all**.

```osy title="a locked entity, and a public one" test app=security-entity-security
entity Secret {
  [MaxLength(100)] string Code;
  // No security block. Nobody reads it, nobody creates it. Locked, by saying nothing.
}

entity Article {
  [MaxLength(200)] string Title;
  // Public — and note you must say WHO: a bare `allow read;` with no `when`/`where` is a compile error.
  security { allow read when IsAuthenticated || IsAnonymous; }
}
```

That is the whole posture in one screen: **silence denies, and only a grant opens.** The failure mode of forgetting a
rule is *"nobody can do it"* — reported within the minute — rather than *"everybody can"*, which nobody reports until
it is somebody else's headline.

### `where` — the owner sees their own   {#where}
A `where` clause is a predicate over the **row**, and `user` is the acting principal. It becomes part of the query:

```osy title="each user sees only their own rows" test app=security-entity-security
[Principal] entity User {
  [Required] string Name;
}

entity Doc {
  User Owner;
  [MaxLength(200)] string Title;
  security {
    allow read where Owner == user;      // I see mine; you see yours
  }
}
```

Under this rule `Doc.Count()` returns a *different number* for different users, and both are correct. That is the
point: the filter is part of the query, not a mask applied afterwards.

### Read and write are separate   {#read-vs-write}
They usually differ — a team can all *read* a memo, but only its author may *change* it:

```osy title="everyone reads; only the owner writes" test app=security-entity-security
entity Memo {
  User Owner;
  [MaxLength(200)] string Note;
  security {
    allow read when IsAuthenticated;     // any signed-in colleague can see it
    allow update where Owner == user;    // only the author can change it
  }
}
```

### Which verbs can I grant?   {#verbs}
A grant names what may be done: **`read`** · **`create`** · **`update`** · **`delete`**. Several may share one rule,
comma-separated — and they routinely differ, which is the point of naming them separately:

```osy title="different people may do different things" test app=security-entity-security
entity Organization {
  [Required, MaxLength(200)] string Name;
  [Required, MaxLength(80)] string Slug;
  security {
    allow read when IsStaff;                     // staff can see every org
    allow create, update, delete when IsAdmin;   // only an admin may change the SET of them
  }
}
```

A rule with no verb list is not a thing you can write: you always say what is being allowed. "Access" is not a
permission — reading is, and deleting is, and they are not the same decision.

### A `where` on `create` — checking the row you are writing   {#create-where}
A `where` filters *existing* rows on **`read`** and on **`delete`**. On **`create`** there is no existing row — so the
same `where` is checked against **the row you are writing**: a create is refused unless the new row satisfies the
predicate. (This is a *WITH-CHECK*, the INSERT half of row-level security.) One predicate governs all four verbs —
*you may only bring into existence a row you would be allowed to own*:

```osy title="a create you may not make is refused, and rolled back" test app=security-entity-security
entity Ledger {
  User Owner;
  [MaxLength(200)] string Note;
  security {
    // The SAME `where` governs create: on the commit path it validates the ROW being written.
    allow read, create, update, delete where Owner == user;
  }
}
```

Here a caller may create a `Ledger` **owned by themselves** (the row satisfies `Owner == user`), but a create whose
`Owner` is someone else is **denied at commit and rolled back** — not saved-then-hidden.

This is the *only* way to scope a create by a column **the app sets itself** (an `Owner` ref, an `Organization` an
app supplies). It is different from the auto-stamped ownership idiom — `allow create when IsAuthenticated` +
`allow read, update, delete where CreatedBy == user.Id` — which is safe *only* because `CreatedBy` is stamped by the
platform and cannot be forged. When the scoping column is one the caller writes, a role-only `when` (which never sees
the row) would let them write it for any tenant; the `create where` is what refuses that.

### An `update` is checked against the row BEFORE **and** AFTER   {#update-both-images}
An update has two versions of the row, and the `where` is asked about **both**:

- **the row as it is** — *may you touch this row at all?*
- **the row as it will be** — *may the row become this?*

Both must pass. Read it as the two halves you already know: `read`/`delete` ask only the first, `create` only the
second (there is no "before"), and `update` — which has both — asks both.

This is the rule that surprises people, so here it is on the shape everybody writes. A timesheet is editable only while
it is a draft:

```osy title="editable while Draft — and what that does and does not permit" test app=security-entity-security
enum SheetStatus { Draft, Submitted }

entity Timesheet {
  [Required] User Owner;
  [MaxLength(140)] string Note;
  SheetStatus Status = SheetStatus.Draft;
  security {
    allow read, create where Owner == user;
    allow update where Owner == user && Status == SheetStatus.Draft;
    // Moving the sheet ON is its own grant, scoped to the one column it changes. Without this line the sheet
    // could never leave Draft, because a row that ends up Submitted is not a row the rule above admits.
    allow update Status where Owner == user;
  }
}
```

With that block:

| the write | why |
|---|---|
| edit `Note` while `Draft` | ✅ both versions of the row are `Draft` |
| edit `Note` once `Submitted` | ❌ the row as it is fails the gate |
| set `Status` to `Submitted` | ✅ through the field-scoped grant, which governs `Status` alone |
| set `Status` back to `Draft` **and** rewrite `Note` in one write | ❌ the row as it *is* is `Submitted`, so the entity-level gate refuses the `Note` — you cannot re-enter the gate to get past it |

⚠ **That last row is the point of asking about both.** If only the after-version were checked, a caller could edit any
row at all simply by setting `Status = Draft` in the same write and carrying every other column with it — and a gate
written that way would stop nobody. If only the before-version were checked, a caller inside the gate could write the
row into a state their own rule forbids.

⚠ **And the third row is why a status gate needs a second rule.** `allow update where … Status == Draft` deliberately
does **not** permit `Draft → Submitted`: leaving the gated state is a different decision from editing inside it, so it
gets its own grant. A [[#field-write|field-scoped rule]] is the usual way to say it, because it *replaces* the
entity-level rule for that one column — everything else stays gated.

### The row is fine and one FIELD is not — `deny read <Field>`   {#field-mask}
Sometimes the ROW is fine and one FIELD is not. A password hash is the canonical case: the login flow must read it to
verify a credential, and **nobody else ever should** — not an admin, not the user themselves, not a support tool, not
an export.

`deny read <Field> when <policy>` masks a single field. The row still comes back; the field does not:

```osy title="a field nobody but the auth flow may read" test app=security-entity-security
// `IsAuthenticator` is the ephemeral principal the login flow runs as (see [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/)).
policy IsAuthenticator => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Authenticator);

entity Credential {
  [Required] User Owner;
  /// Salted hash of the password — never the plaintext.
  [MaxLength(200)] string PasswordHash;
  security {
    allow read when IsStaff;                        // staff read the directory …
    deny read PasswordHash when !IsAuthenticator;   // … but the hash, only the auth flow, ever
    allow update where Owner == user;               // a user maintains their own credential
  }
}
```

This is worth reaching for more often than people do. A field mask is a **narrow, auditable** statement — *this
column, to these people, never* — and it survives every future query, endpoint, export and tool automatically,
because it is enforced where the data is read rather than wherever someone remembered to be careful.

### A mask can filter by ROW too — `deny read <Field> where <row predicate>`   {#field-mask-row-scoped}
`deny read <Field> when <policy>` answers ONE question for the whole request: *"is this caller the kind of person who
may see the column at all?"* — a `when` has no row in scope, so it can never depend on WHICH row. Sometimes the real
rule needs the row too: *"visible to the account's own person, or to Finance"* is a per-row exception a `when` alone
cannot express.

A field mask takes a `where` for exactly this, evaluated per row the SAME way a row-level rule's `where` is:

```osy title="the account number: its owner sees it, an admin sees every one, nobody else sees any" test app=security-entity-security
entity BankAccount {
  [Required] User Owner;
  [MaxLength(34)] string? Iban;
  security {
    allow read when IsStaff;
    deny read Iban where !IsAdmin && Owner != user;   // an admin, or the account's own person — nobody else
  }
}
```

A declared `policy` binds as a Boolean in a field mask's `where` exactly as it does in a row rule's, so `!IsAdmin &&
Owner != user` reads as ordinary C#-shaped logic: masked unless the caller is privileged, or it is their own row. The
`where` may equally read the row's OWN columns with no policy at all (`deny read Notes where Owner != user;`) — a raw
predicate, nothing to declare.

**The mask is still exclusionary only, never a grant.** It can only ever HIDE more of what the row-level rule already
allowed — it cannot expose a column on a row the row-level rule has already excluded. The row filter decides whether
there is a row to talk about at all; a field mask never runs ahead of it, and there is no caller for whom a mask
"opens" a row nobody's `allow read` granted them.

**When to split the field into its own entity instead.** If the sensitive column wants a genuinely INDEPENDENT
security posture — its own auditing, its own write rule, a lifecycle of its own — give it its own entity with an
ordinary row rule (`allow read where IsAdmin || Owner == user;`) rather than masking it on the parent. Both are
correct; the split earns its keep when the field is not merely hidden but actually governed differently from the
rest of its row.

### `allow read <Field>` — lifting the mask again   {#field-unmask}
A field rule on **`read`** edits ONE thing: the set of columns to hide. **`deny read <Field>` puts a column into that
set; `allow read <Field>` takes it back out.** So the two spell the same idea from opposite ends, and the second is
useful when the honest default is *"nobody"*:

```osy title="hidden from everyone by default, then lifted for the people who may see it" test app=security-entity-security
entity Applicant {
  [Required, MaxLength(200)] string Name;
  /// Interview notes — written about a person, read by the panel and nobody else.
  [MaxLength(2000)] string Notes;
  security {
    allow read when IsAuthenticated;   // the applicant row is ordinary, visible to the company …
    deny  read Notes;                  // … the notes are hidden, from everyone, with no exception …
    allow read Notes when IsStaff;     // … except the panel
  }
}
```

And that is a claim about BEHAVIOUR, so it is executed rather than asserted in prose:

```osy title="proof: the row arrives, the column arrives only for the panel" run app=security-entity-security
principal Panelist  => User.Single(u => u.Name == "Pat");
principal Colleague => User.Single(u => u.Name == "Sam");

[TestFixture]
void SeedApplicants() {
  var pat = new User { Name = "Pat" };
  var sam = new User { Name = "Sam" };
  var panel = new RoleGrant { Grantee = pat, Level = AppRole.Staff };
  var robin = new Applicant { Name = "Robin", Notes = "strong on systems" };
}

[Test(SeedApplicants)]
[runas(Colleague)]
void A_colleague_gets_the_row_but_not_the_notes() {
  var a = Applicant.Single(x => x.Name == "Robin");
  Assert.Equal("Robin", a.Name);       // the ROW comes back …
  Assert.Null(a.Notes);                // … and the masked column does not
}

[Test(SeedApplicants)]
[runas(Panelist)]
void The_panel_gets_the_notes() {
  var a = Applicant.Single(x => x.Name == "Robin");
  Assert.Equal("strong on systems", a.Notes);   // the lift, for the callers it names
}
```

Written the other way round — `deny read Notes when !IsStaff` — that is one line instead of two and it means the same
thing here. Prefer whichever states the intent you would defend in review: an unconditional `deny` plus an explicit
lift says *"nobody, and here is the exception"*, which is the safer sentence when the exception list is likely to
grow.

⚠ **An `allow read <Field>` is NOT a grant, and the compiler will not let you write it as one.** It cannot make a
column appear in a row the entity's `read` rules withhold — whether the ROW comes back is decided by those rules and
by them alone, and a field rule takes no part in it. So an `allow read <Field>` with no `deny read <Field>` beside it
has nothing to lift, and is refused at compile time rather than accepted as an inert line:

```osy title="what the compiler says when the rule can do nothing" syntax
entity Vendor {
  [MaxLength(200)] string Email;
  security {
    allow read when IsStaff;
    allow read Email when IsAnonymous;   // ✗ `allow read Email` does nothing on 'Vendor'.
  }                                      //   There is no `deny read Email` here, so this line has
}                                        //   nothing to lift and no caller's access changes.
```

To open a column to a wider audience you widen the **row** rule (`allow read when …`) and mask what should stay
narrow. That is the only direction reading works in.

### A field rule on a WRITE verb replaces, rather than masks   {#field-write}
On `create`/`update`/`delete` a field rule means something different again: when a column has any rule of its own,
**that rule REPLACES the entity's for that column** — the entity's grant simply does not reach it. This is how you
carve one writable column out of an otherwise read-only row, and it stands alone (there is nothing to lift, so no
sibling `deny` is needed):

```osy title="one column the login flow may stamp, on a row nobody else may touch" test app=security-entity-security
entity DeviceSession {
  [Required] User Owner;
  [MaxLength(80)] string Device;
  DateTime? LastSeenAt;
  security {
    allow read when IsAuthenticated;
    allow update LastSeenAt when IsAuthenticator;   // ONLY this column, ONLY the auth flow
  }
}
```

Nobody may update `Device` — the entity grants no `update` at all — and `LastSeenAt` is governed by its own line
rather than by the (absent) entity rule. `osy explain` prints both halves of this, per column, in the words above.

### `when` — gate by who is asking   {#when}
A `where` asks *"is this row yours?"*. A `when` asks *"are you the kind of person who may do this at all?"*. Name that
test with a `policy` and reuse it:

```osy title="a role policy, and the rule that uses it" test app=security-entity-security
enum AppRole { Staff, Admin, Authenticator }

entity RoleGrant {
  User Grantee;
  [Required] AppRole Level;
}

policy IsStaff => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Staff);
policy IsAdmin => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Admin);

entity Report {
  [MaxLength(200)] string Title;
  security {
    allow read when IsStaff;             // staff read every report; nobody else reads any
  }
}
```

A `policy` is written once and referenced everywhere, so the definition of "staff" lives in one place. When it
changes, it changes everywhere — which is the only way it stays true.

### `when` and `where` have the same freshness   {#same-freshness}
The two ask different **questions**. They do not ask them at different **times**. Whichever you write, the answer is
computed against the grants as they stand when the read or the write happens — so a grant your own code committed a
moment ago is already in force, and a grant it revoked is already gone. The same is true of a
[[#field-mask|field mask]], which is the third way of writing the same authorization.

That matters because the two spellings are often the same rule. `when IsStaff` and
`where RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Staff)` say one thing twice — `IsStaff` **is** that
expression, named — and nothing about your app should change depending on which you reached for.

```osy title="a grant committed mid-request is in force for the rest of it" test app=security-entity-security
// Onboarding, founding, an invite accepted: one request writes the grant and then does the work the grant
// authorizes. Both spellings see it, and so does a field mask — there is no snapshot taken when the request began.
void JoinTheStaffAndGetToWork() {
  var g = new RoleGrant { Grantee = Session.CurrentUser, Level = AppRole.Staff };
  UnitOfWork.Commit();          // ← the grant is a fact now …

  var mine = Report.Count();    // … and `allow read when IsStaff`, two blocks up, already knows it
}
```

It runs the other way too, and that direction is the one worth stating: a role **removed** mid-request stops working
for the rest of that request. An admin screen that revokes a membership and then re-renders beneath it is showing the
access the person has now, not the access they had when the page began.

⚑ **This covers ORDINARY ROWS, not just grants — which matters most for a gate that reaches through a reference.**
`allow update where Report.Employee.User == user && Report.Status == ReportStatus.Draft` is decided against the
report as it is *now*, every time it is asked. So the flow an expense app is built out of behaves the way you would
read it: add a line while the report is a draft, submit the report, and the next edit of that line is refused — in
the same request that submitted it, with no re-login, no new page and no second context. Recall the report and the
same edit is allowed again. Nothing is snapshotted when the request begins, and nothing is remembered from the
first time the rule looked at the parent.

⚠ **The bound is the READ, not the request.** A change committed somewhere else — an admin in another session, a
background job, your own API — is in force from the next read that asks. It is not pushed to anything already on
screen: a page does not lose a button the instant a role is revoked; it loses it the next time it asks the server
anything. So this is "authorization follows the facts", not "live revocation across a system".

### Test it, or you have not written it   {#test-it}
A security rule you have not tested is a rule you *believe* you wrote. Prove it, with [`runas`](https://osysharp.com/reference/testing/runas/) and
[`Assert.Denied`](https://osysharp.com/reference/testing/assert/):

```osy title="prove the rule denies the person it should" run app=security-entity-security
// A `principal` names a seeded row so a test can BE that person. It resolves unsecured, which is what makes
// it work: a `[Test]` body outside a `runas` is an anonymous caller, so looking a user up there reads nothing.
principal Bob => User.Single(u => u.Name == "Bob");

[TestFixture]
void Seed() {
  var alice = new User { Name = "Alice" };
  var bob = new User { Name = "Bob" };
  var aliceDoc = new Doc { Owner = alice, Title = "alice-doc" };
}

[Test(Seed)]
[runas(Bob)]
void Bob_cannot_see_Alices_doc() {
  Assert.Equal(0, Doc.Count());         // not "hidden" — for Bob, the row does not exist
}
```

Write one of these for every rule that matters. It is what stops a refactor six months from now from quietly opening a
door nobody notices is open.

### `message` — what a refused person is told   {#message}
A block may open with one line of your own copy, and it is the sentence anyone refused a `create`, `update` or
`delete` on this entity reads:

```osy syntax
security {
  message "Only an organisation admin can change an approval policy.";
  allow create, update, delete when IsOrgAdmin;
}
```

It covers the refusal however it was reached — the row matched none of the `allow` rules, or none of them is active
for this caller — which is the case no per-rule `message` can speak for, because the refusal was made by the ABSENCE
of a matching rule. Usually you do not write it per entity at all: `app.DenialMessage = "…";` says it once for the
whole app and each entity overrides only if it needs different words. Reading is not covered and cannot be — a read
you may not do is filtered, not refused. The whole story is in [what a refused user is told](https://osysharp.com/reference/security/denial-messages/).

## See also       {#see-also}
- [what a refused user is told](https://osysharp.com/reference/security/denial-messages/) — the sentence a refused person reads, and how to write it yourself
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — what an entity permits before you write any block
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — the `user` a rule compares against
- [runas](https://osysharp.com/reference/testing/runas/) — acting as a principal, so a rule can be tested
- [Assert](https://osysharp.com/reference/testing/assert/) — `Assert.Denied`


---

<!-- https://osysharp.com/reference/security/invitation-signup/ -->

# signup by invitation (invite, accept link, chase, expire)

> How an app lets somebody INVITE a person who has no account yet. The invitation is a workflow: it mints a tokenised accept link the invitee can answer with no login, chases them on a cadence if they do not, and expires on its own if they never do. Accepting mints the account AND its password in one step, so it is never claimable by anyone else. Nothing here is a cron job, a sweep, or a `LastNudgedAt` column.

<!-- id: security-invitation-signup · area: security · stability: preview · html: https://osysharp.com/reference/security/invitation-signup/ -->

## Summary        {#summary}
Almost every application needs this and it is always the same shape: somebody invites an address, the person at that
address gets a link, and clicking it makes them a user. The hard parts are not the invite — they are everything
around it. What if they never answer? What if they answer twice? What if they have no account to answer *with*?

In Osy# the invitation **is a workflow over the invitation row**, and that answers all three:

| the awkward part | what carries it |
|---|---|
| they have no account, so they cannot sign in to accept | [`<Slot>.CallbackUrl()`](https://osysharp.com/reference/workflow/callback-url/) — a link that IS the permission |
| they have not answered, and somebody has to chase them | [`Remind`](https://osysharp.com/reference/workflow/remind/) on the milestone — the run chases itself |
| they never answer, and it must not sit open for ever | the milestone's breach arm — [`Unfinished { goto Expired; }`](https://osysharp.com/reference/workflow/milestone/) |
| an admin wants to see who is outstanding | [`Workflow.WorkByItem<Invitation>()`](https://osysharp.com/reference/workflow/work-by-item/) + the audit trail |

**No cron, no sweep, and no bookkeeping columns.** The instinct is to grow `LastNudgedAt`, `NudgeCount` and
`ExpiresAt` on the invitation and a job to maintain them. The engine already holds the clock and already records
every reminder it fired, so the app reads them instead of keeping its own copy that can drift.

## Signature      {#signature}
```osy syntax
state Pending {
  subscribe Accept(string passwordHash) as Acceptance {
    Finished {
      Within = <TimeSpan>;                       // how long they have
      Remind Chase(After = …, ThenEvery = …) { } // how they are chased before that
      Unfinished { goto Expired; }               // what happens if they never answer
    }
  }
  enter { this.Item.AcceptLink = Acceptance.CallbackUrl(); }   // the link the email points a page at
  on Acceptance(string passwordHash) { /* mint the account AND its credential */ goto Active; }
}
```

## Description    {#description}

### The whole flow, compiled   {#the-flow}
`Invitation` is an ordinary entity; the workflow tracks its status. `Autostart` means creating the row starts the
run, so "invite this address" is one `new Invitation { … }` and nothing else.

```osy title="an invitation that chases itself" test app=security-invitation-signup
enum InviteStatus { Pending, Active, Revoked, Expired }

[Role] enum AppRole { Authenticator, Member, Admin }

[Principal]
entity Account {
  [Required, MaxLength(100)] string Name;
  [Required, MaxLength(200)] string Email;
  // Nullable because the SEEDED accounts in a fixture may have none. An account minted by ACCEPTING always has its
  // hash from the moment it exists — see `on Acceptance` below, and the section on why that matters.
  [MaxLength(200)] string? PasswordHash;
  security {
    allow read when IsAuthenticated;
    allow read, create, update when IsAuthenticator;
    deny read PasswordHash when !IsAuthenticator;
  }
}

entity RoleGrant {
  [Required] Account Grantee;
  [Required] AppRole Level = AppRole.Member;
  security { allow read when IsAuthenticated; allow create when IsAuthenticator; }
}

entity Invitation {
  [Required, MaxLength(200)] string Email;
  InviteStatus Status;
  // Where the minted link is kept so the app can mail it. `CallbackUrl()` returns the plaintext ONCE — only its
  // hash is stored — so the body that mints it is the only place it can be put anywhere.
  [MaxLength(500)] string? AcceptLink;
  security { allow read, create, update when IsAuthenticated; }
}

// The auth flow runs as the EPHEMERAL Authenticator — no user yet — so it needs its own grant to reach the
// credential rows it was called to check.
policy IsAuthenticator => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Authenticator);
policy IsAdmin         => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Admin);

workflow Onboarding {
  Tracks    = Invitation.Status;
  Autostart = true;
  Initial   = Pending;

  // Only the person it was addressed to may accept from INSIDE the app — an identity comparison, per row.
  [Authorize(u => u.Email == this.Item.Email)]
  event Accept(string passwordHash);

  // Revoking is an office, so it is a grant lookup: per person, the same answer on every invitation.
  [Authorize(u => RoleGrant.Any(g => g.Grantee == u && g.Level == AppRole.Admin))]
  event Revoke();

  on Revoke { goto Revoked; }

  state Pending {
    subscribe Accept() as Acceptance {
      Finished {
        Within = TimeSpan.FromDays(7);
        Remind Chase(After = TimeSpan.FromDays(3), ThenEvery = TimeSpan.FromDays(2)) {
          Nudge(this.Item);
        }
        Unfinished { goto Expired; }
      }
    }

    enter { this.Item.AcceptLink = Acceptance.CallbackUrl(); }

    on Acceptance(string passwordHash) {
      // The deposit may have arrived with NOBODY signed in, so accepting is what mints the account — WITH its
      // credential, in one step, so there is never a moment when it can be claimed by somebody else.
      var existing = Account.Where(a => a.Email == this.Item.Email).FirstOrDefault();
      if (existing == null) {
        var minted = new Account { Name = this.Item.Email, Email = this.Item.Email, PasswordHash = passwordHash };
        new RoleGrant { Grantee = minted, Level = AppRole.Member };
      }
      this.Item.AcceptLink = null;     // the deposit burned the token; do not advertise a dead link
      goto Active;
    }
  }

  terminal success Active  { }
  terminal cancel  Revoked { Message = "invitation revoked"; }
  terminal error   Expired { Message = "invitation expired"; }
}

void Nudge(Invitation i) {
  Log.Information("invitation reminder — {Email} has not accepted yet", i.Email);
}
```

### The link is the permission     {#the-link}
`Acceptance.CallbackUrl()` mints an absolute URL for **that one slot on that one run**. The invitee answers it by
POSTing to it — with no account, no session and no sign-in:

```text
POST https://myapp.example.com/api/workflow/callback/LBuW9YC_9YEaXtku…
```

The body is the event's parameters as a JSON object, by name — here `{"passwordHash": "…"}`. An event with no
parameters is answered by posting nothing at all.

⚠ **The invitee never does this by hand.** A callback URL is answered by a POST and an email link is a GET, so the
address in the email is a page of yours that carries the token and POSTs on submit — see [below](#landing).

⚠ **The event's `[Authorize]` does not govern this door, and cannot.** A predicate takes a principal and a callback
deposit has none — that is the whole point of the feature. What stands in its place is the token: 256 bits, stored
only as a hash, single-use, scoped to one slot on one run, and dead the moment the slot closes. So *whoever can read
the invitee's mail can accept the invitation* — which is exactly the authority a real invite link carries, and it is
worth knowing that you are choosing it. The full contract is in [Callback URLs — letting an outsider complete one slot](https://osysharp.com/reference/workflow/callback-url/).

### The link goes to a PAGE, not to the callback endpoint     {#landing}
A callback URL is answered by a **POST**, on purpose — a link that acted on being *fetched* would be spent by the
first mail scanner or link preview that touched it. An email link is a **GET**. So the address in the email is a page
of your own, which carries the token and does the POST when the invitee submits:

```osy syntax
[Page("/accept/{token}")] [AllowAnonymous]      // whoever opens it has no account and cannot sign in
component AcceptInvite(string token) { … collect a password, then call the function below … }
```

### Accepting mints the account WITH its password, in one step     {#credential}
⚠ **This is the part that is easy to get wrong, and getting it wrong is an account takeover.** The tempting shape is:
the arm mints an `Account` with no `PasswordHash`, and the invitee sets one later at the signup form, which "claims"
the credential-less row. **Do not build that.** A row with no password is a row anybody who knows the address can
claim — and an invitation is precisely where an attacker knows the address. The token proved that this person
controls this mailbox, and the claim throws that proof away.

Mint the account and its credential together, authorized by the token, so the window never opens:

```osy title="the token mints the account and its password together" syntax
// the landing page's one call
string AcceptInvitation(string token, string password) {
  try {
    // Hash FIRST, then answer the link: an event argument is not a safe place for a plaintext, because a body that
    // parks (a retry, an await) persists its args to resume with.
    // `token` is the last segment of the mailed AcceptLink; Redeem takes that or the whole URL ([Workflow.Redeem (answer a callback URL, as nobody)](https://osysharp.com/reference/testing/redeem-callback/)).
    Workflow.Redeem(token, "{\"passwordHash\": \"" + Security.HashPassword(password) + "\"}");
    return "";
  }
  catch (NotFoundException e) { return "This link is not valid, or it has already been used."; }
  catch (ConflictException e) { return "This invitation is no longer open."; }
}
```

⚑ **The token is the only authority here, and the address is never taken from the request** — the arm reads it off
`this.Item`, the invitation the token addresses. So a caller holding a valid link cannot aim it at somebody else's
invitation, and one holding no link can do nothing at all. That is why this needs no `[Authorize]` and no signed-in
user: there is nobody to authorize.

And `Signup` stays **create-only**:

```osy title="signup stays create-only, so there is nothing to claim" syntax
[AuthMethod]
string Signup(string email, string password) {
  if (Account.Any(a => a.Email == email)) { return ""; }   // taken — sign in, or use your invitation link
  var a = new Account { Name = email, Email = email, PasswordHash = Security.HashPassword(password) };
  var grant = new RoleGrant { Grantee = a, Level = AppRole.Member };
  return Security.IssueJwt(a.Id, a.Email);
}
```

There is nothing here to claim, because a credential-less account is not a state the app ever has.

### Two doors into one slot     {#two-doors}
`Acceptance` is satisfied either by the emailed link (anonymous, over its token) or by an Accept button inside the
app (a signed-in invitee, over the `[Authorize]`). Whichever arrives first satisfies it; the second finds it closed
and is told so. You do not choose between them — one slot serves both.

### How do I watch the pending invitations?     {#desk}
[`Workflow.WorkByItem<Invitation>()`](https://osysharp.com/reference/workflow/work-by-item/) gives one row per invitation with a **live** run: how
long is left, and what governs. The chase count and the breach come off the run's [audit trail](https://osysharp.com/reference/workflow/audit/):

```osy syntax
foreach (var a in Onboarding.For(i).Audit) {
  if (a.Kind == AuditKind.Reminded) { nudges = nudges + 1; }
  if (a.Kind == AuditKind.Breached) { everBreached = true; }
}
```

⚠ **Read the breach off the TRAIL, not off the work row, when the breach ENDS the run.** `WorkByItem` is one row per
*live* run — and an invitation whose deadline lapsed `goto Expired`, so its row and its `EverBreached` disappear in
the same sweep that made the answer true. (`WorkByItem.EverBreached` is for the other shape: a promise missed on a
run that stays open, like a support ticket still owed an answer.) The audit trail has no such horizon.

### Resending, and revoking     {#resend}
Calling `CallbackUrl()` again for the same slot issues a **new** link and retires the old one — which is what a
resend must do, so that correcting a typo'd address does not leave the first address able to answer. Revoking is an
ordinary workflow-level route, so it works from any non-terminal state.

## Examples       {#examples}
The complete, running application this page is drawn from is **`demo/wf-signup-invite`** — model, tests, a login
page and an invite desk that shows each invitation's countdown, its chase count and its live accept link. Run it:

```bash
cd demo/wf-signup-invite
osy test
osy user add ada@corp.test --role Admin --password demo1234 --set Name=Ada
osy import --as ada@corp.test --password demo1234
osy launch
```

Then accept one the way an invitee with no account would — by POSTing to the link the desk is showing:

```bash
curl -X POST 'http://wfsignupinvite.localhost:8156/api/workflow/callback/<token>' \
     -H 'Content-Type: application/json' -d '{"passwordHash": "<the hash the accept page computed>"}'
```

The invitation goes Active, an `Account` appears for that address **with the credential the POST carried** — never
without one, as `#credential` above insists — and the link is retired.
POST it a second time and it is `404` — the deposit burned it.

## See also       {#see-also}
- [Callback URLs — letting an outsider complete one slot](https://osysharp.com/reference/workflow/callback-url/) — the link's full security contract, and what it does *not* relax
- [Remind (milestone reminders)](https://osysharp.com/reference/workflow/remind/) — the chase, its cadence, and why a missed cadence is coalesced rather than replayed
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — `Within` and the breach arm that ends the run
- [Workflow.WorkByItem&lt;T&gt; (one row per item — the board read)](https://osysharp.com/reference/workflow/work-by-item/) — the per-item board the desk is built on
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — how `Signup`/`Login` are wired as the app's auth methods
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — the grant table the accept arm writes into


---

<!-- https://osysharp.com/reference/security/denial-messages/ -->

# what a refused user is told

> When a write is refused, two different sentences are produced from it. The person using your app gets a short one that names the operation and the thing and no more — never the rule, the predicate, or their own address. Everyone building the app — `osy test`, `osy run`, `osy logs` — gets the whole story: entity, verb, the rule as you wrote it, and who was asking. You write the first one, in your own words: once for the whole app with `app.DenialMessage`, per entity with `security { message "…"; }`, or per condition on a `deny` rule.

<!-- id: security-denial-messages · area: security · stability: stable · html: https://osysharp.com/reference/security/denial-messages/ -->

## Summary        {#summary}
A refusal is read by two audiences with opposite needs, so the platform writes it twice.

| Who | Sees | Where |
|---|---|---|
| the person using your app | **"You do not have permission to change this policy."** | the 403 body, the error toast, a REST or MCP error |
| whoever is building it | that sentence **plus** entity, verb, the rule as you wrote it, and the caller | `osy test`, `osy run`, `osy query`, `osy logs` |

You never choose between them and you never wire anything up. What you *do* choose is the first sentence — the one
your users read — and the usual way to choose it is one line for the whole app:

```osy title="the app's own voice, for every entity in it" test app=security-denial-messages
app.DenialMessage = "You do not have permission to {verb} the {entity}.";

[Principal] entity Member { [MaxLength(200)] string Email; }
```

That sentence is now what a refused person reads anywhere in the app, with `{verb}` and `{entity}` filled in per
refusal: *"You do not have permission to update the cost center."* An entity that needs different words says so
itself; a single condition worth explaining says so on its own rule. Those are the exceptions, in that order.

⛔ **This is about WRITES — `create`, `update`, `delete` — and reads are not missing from that list.** A read you are
not permitted is not refused, it is **filtered**: the row is simply not in the result. There is no denial, so there
is nothing to say. See [Why reading is not here {#reads}](#reads).

## Signature      {#signature}
```osy syntax
app.DenialMessage = "…";                     // the whole app. One line, usually the only one you need.

security {
  message "…";                               // this entity, when the app's sentence is not right for it
  deny <ops> [when …] [where …] message "…";  // this CONDITION, when the condition is the person's situation
}
```

Most specific wins. A `deny` rule's own message beats the entity's, which beats the app's, which beats the
platform's built-in sentence.

### The holes a message can carry        {#placeholders}

| Hole | Becomes | Example |
|---|---|---|
| `{verb}` | `create`, `update` or `delete` | "you cannot **update** …" |
| `{Verb}` | the same, capitalised | "**Update** is not allowed …" |
| `{entity}` | the entity's name read back as English | `CostCenter` → "cost center", `HTTPEndpoint` → "HTTP endpoint" |
| `{Entity}` | the same words, sentence-cased | `CostCenter` → "Cost center" |

Write `{{` for a literal brace. Anything else in braces is a **compile error** naming this whole list, so a typo
never reaches a real person as `{entty}`.

⚠ **The article is yours.** The platform's own sentence says "this cost center" rather than "a cost center" because
no rule picks *a*/*an* correctly — a vowel-letter rule writes "an user", a consonant one "a invitation". Your app
knows its nouns, so write "the {entity}", "a {entity}", or drop the article entirely.

## Description    {#description}

### One sentence for the whole app        {#app-level}
`app.DenialMessage` is a top-level line in any model file. It is the whole surface most apps ever need: the
platform's built-in wording is nearly right already, and what an app actually wants is to say the same thing in its
own voice, once.

```osy title="an app that speaks for itself" test app=security-denial-messages
entity CostCenter {
  [MaxLength(100)] string Title;
  security {
    allow read when IsAuthenticated;
    allow create, update, delete when IsAuthenticated;
  }
}
```

With the `app.DenialMessage` above, a refused write on `CostCenter` reads *"You do not have permission to update the
cost center."* — for every verb, and however the refusal was reached.

**However it was reached** is the part that matters, because there are two ways and an author should not have to
know which one they hit:

- the row satisfied none of the `allow` rules (*"this row is not yours"*), or
- no rule for that verb is active for this caller at all (*"nobody in your position may do this"*).

Both get your sentence.

### When one entity needs different words        {#entity-level}
Put a `message` at the top of that entity's `security { }` block. It overrides the app-level sentence for this
entity only, and takes the same holes.

```osy title="an entity whose refusal deserves its own wording" test app=security-denial-messages
entity Policy {
  [MaxLength(100)] string Name;
  Member Owner;
  security {
    message "Only an organisation admin can {verb} an approval policy.";
    allow read when IsAuthenticated;
    allow create, update, delete where Owner == user;
  }
}
```

Reach for this when the app-level sentence would be misleading or unhelpfully vague for one thing in particular —
a screen users hit often, a rule that surprises people, a noun the generic sentence reads badly around. If you find
yourself writing near-identical messages on entity after entity, that is the app-level line asking to be written
instead.

### When a single condition is the whole story        {#rule-level}
A `message` on a `deny` rule replaces the sentence **for that rule only**, and only when that rule is what refused.
Use it where the CONDITION genuinely *is* the person's situation and knowing it is what they need:

```osy title="a condition worth explaining, said in your words" test app=security-denial-messages
entity Report {
  [MaxLength(120)] string Title;
  Member Owner;
  bool Locked = false;
  security {
    message "Only the owner can {verb} a report.";
    allow read when IsAuthenticated;
    allow update where Owner == user;
    // The one case that is not "you are the wrong person" — it is "this is the wrong time".
    deny update where Locked message "This report is locked while it is being paid. It reopens once payment clears.";
  }
}
```

Someone editing a locked report is told about the lock. Everyone else refused an update on a `Report` is told *"Only
the owner can update a report."*

⚠ **This is the narrow tool, not the general one — and a `message` on an `allow` is a compile error.** A predicate
says who QUALIFIES, from the system's point of view; it never becomes a description of the person's situation
however you word it. And the commonest refusal of all is made by the ABSENCE of a matching rule — several `allow`s
were consulted and none matched — so no individual rule's message would even be true. That is what the entity-level
and app-level messages are for.

### Why reading is not here        {#reads}
There is no way to write copy for a refused read, and that is a design decision rather than a gap.

Reading is **filtered, not refused**. A query returns the rows you may see; the rest are not in the result. There is
no denial, no 403, and nothing that happened for a sentence to describe — so a message would have nowhere to appear
and nothing to be about. You do not see what you are not allowed to.

### What your users read when you have written nothing        {#user-copy}
The built-in sentence, built from the operation and the entity's own name read back as English:

| Operation | Sentence |
|---|---|
| create | You do not have permission to create this cost center. |
| update | You do not have permission to change this cost center. |
| delete | You do not have permission to delete this cost center. |

When the refusal names no entity at all, it is *"You do not have permission to do that."*

**Nothing else ever appears there.** Not the rule, not the predicate, not the name of a policy, not the caller's own
email address. That is deliberate on both counts: a denial message is exactly where somebody probes, so the rules are
not printed at it; and a sentence naming an internal is not copy your users should ever have been shown.

⚠ **It does not narrate the rule either.** "You must be an admin of this organisation" would be friendlier and it is
still the rule, in nicer words. What a person may DO about a refusal is your app's to say — which is what everything
above this section is for.

### Where the whole story is        {#builder-detail}
Writing friendly copy never costs you the ability to debug your own app. Every refusal is recorded server-side in
full, at `Warning`, with the entity, the verb, the caller and the request's correlation id as fields — so a person
who reports *"it says I don't have permission"* hands you the correlation id from the error they saw, and one command
shows you exactly which rule refused them:

```console
osy logs --correlation <id>
```

The same detail is inline in the places you are already looking while you build:

- **`osy test`** — a denial that escapes a test uncaught reports both halves.
- **`osy run`, `osy query`** — you are driving your own app as its author, so you get both halves.
- **`osy explain`** — reads the rules directly; see [security { }](https://osysharp.com/reference/security/entity-security/).

⚑ A refusal is very often the security model working exactly as it should — a screen offering a button it should not,
a probe, a grant that has lapsed. That is why it is recorded at `Warning` rather than as an error: a log where every
correct refusal looks like a fault is a log where the real fault is invisible.

## See also       {#see-also}
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `security { }` block these messages live in
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — why an entity you say nothing about refuses everyone
- [runas](https://osysharp.com/reference/testing/runas/) — becoming the refused user, which is the only way to know a rule works
- [Seeing what happened when your code ran](https://osysharp.com/reference/local/seeing-what-happened/) — following a correlation id back through the whole request


---

<!-- https://osysharp.com/reference/stdlib/culture-formatting/ -->

# Culture formatting — ToString(format, culture)

> Format numbers, currency, percentages and dates for a declared culture — `total.ToString("C", "sv-SE")` → `1 234,56 kr`. A per-call culture overrides; `app.DefaultCulture` sets an app-wide default. Supported formats run in the browser, byte-identical to the server; everything else runs server-side.

<!-- id: stdlib-culture-formatting · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/culture-formatting/ -->

## Summary        {#summary}
Format a number or date for a **declared culture**, spelled exactly as C#'s `IFormattable.ToString(format, provider)` —
the provider is a BCP-47 culture token (`"sv-SE"`, `"de-DE"`), not an ambient setting:

```osy syntax
var price = total.ToString("C", "sv-SE");     // "1 234,56 kr"  — Swedish krona, space grouping, comma decimal
var pct   = rate.ToString("P1", "en-US");     // "12.5 %"
var when  = order.CreatedAt.ToString("D", "de-DE");   // "Freitag, 5. Januar 2024"
```

The culture must be **declared** (a `cultures { }` set; the platform ships a default pack, so `sv-SE`/`de-DE`/… work
with no ceremony). A per-call culture is the override; `app.DefaultCulture` sets the app-wide default so a culture-less
`total.ToString("C")` still formats for it.

## Signature      {#signature}
```osy syntax
string  value.ToString(string format)                  // the app-default culture (else invariant)
string  value.ToString(string format, string culture)  // an explicit culture — the per-call override
// declared once, app-wide:
app.DefaultCulture = "sv-SE";
// per-viewer (on the principal entity) + the read:
[PreferredCulture] Culture Locale;
string cur = Session.CurrentCulture;                   // the current viewer's effective culture token
```

## Description    {#description}
**The culture is explicit, never ambient.** There is no "current culture" read from the machine or the request — a
value formats for the culture you name, or for the app default. This is what makes a price render the same on the
server and in every browser.

**Declared, closed set.** A culture token on a format call must belong to the app's declared `cultures { }` set. The
platform ships a default pack (`en-US`, `en-GB`, `sv-SE`, `de-DE`, `fr-FR`, `es-ES`, `it-IT`, `nl-NL`, `pt-BR`,
`ja-JP`, `zh-CN`); an app can vendor the block to curate. A token outside the set is a **compile error** (with a
"did you mean").

**Supported formats (run in the browser, no round trip).** Over a `Decimal`, `Int32`, `Double` (fixed specifiers only),
`DateTime`, `DateOnly` or `TimeOnly`:

| Kind | Formats | Example (sv-SE) |
|---|---|---|
| Number | `N` `F` `D` (+ digits: `N2`, `F0`) | `1 234,56` |
| Currency | `C` (+ digits) | `1 234,56 kr` |
| Percent | `P` (+ digits) | `12,5 %` |
| Date/time | `d D t T g G M Y` | `2024-01-05` · `fredag 5 januari 2024` |
| Custom numeric | `#,##0.00`, `0.00` | `1 234,56` |
| Custom date | `yyyy-MM-dd`, `dd/MM/yyyy`, `MMMM d` | `2024-01-05` |

The client reproduces .NET exactly — the culture's separators (including a non-breaking-space group separator and a
`−` minus), currency symbol and placement, month/day names (with the **genitive** forms .NET selects when a day number
is adjacent, e.g. German `5. Juni` vs standalone `Jun`), and AM/PM.

A **`Double`** formats client-side too for the fixed specifiers (`N`/`F`/`C`/`P`) — the client rounds the value's
*exact* decimal expansion (a double is a terminating decimal), reproducing .NET including its type-specific rounding
(a double rounds half-to-even, `(2.5).ToString("F0")` → `"2"`, where a `Decimal` rounds half-away-from-zero). Prefer
`Decimal` for money regardless — it is exact end to end.

**Everything else runs on the server** (correct, one round trip): **parsing** a string back to a value; the numeric
`E`/`G`/`R`/`X` specifiers, a *custom pattern over a double*, and a double's plain `.ToString()` — these need
shortest-round-trip / 15-digit rounding that isn't reproduced client-side; non-Gregorian date calendars/eras under a
culture; and any format built at runtime.

**App default.** `app.DefaultCulture = "sv-SE";` makes a culture-**less** `value.ToString("C")` format for `sv-SE`
everywhere — server and client — without repeating the culture at each call. A per-call culture still overrides it. With
no default declared, a culture-less format is culture-neutral (invariant), unchanged.

**Per-viewer culture.** Give the principal entity a `Culture` property marked `[PreferredCulture]`, and a culture-**less**
`value.ToString("C")` renders in *the current signed-in user's* culture. `Culture` is a value-kind that stores a BCP-47
token (`"de-DE"`) — a picker or a plain string sets it — validated to be a real culture when written. The resolution
order is: **a per-call culture > the viewer's `[PreferredCulture]` > `app.DefaultCulture` > invariant**. It resolves at
runtime and, like every other supported format, runs **in the browser** (the viewer's culture is served with the app's
metadata, so there is no round trip); a viewer whose preferred culture is not one the app declared falls back to the
app default.

**A number typed into a box follows the same order.** `NumberField` and `DecimalField` — and any `Field` bound to a
numeric member — render and accept their digits in the viewer's effective culture, not the browser's. So under
`app.DefaultCulture = "sv-SE"` the box shows `12,5` and takes `12,5`, `1 234,50` and `-12,5`, and the member behind it
still holds an exact `decimal`. A number written with another culture's decimal mark is not a number: `12.5` there
answers what an empty box answers, rather than being read as `125` or as `12.5` — either of which would be a wrong
number nobody could see.

`min`/`max` on the numeric controls mark the box invalid the moment the value falls outside, so the reader sees it as
they type rather than when the save is refused.

**Reading the viewer's culture.** `Session.CurrentCulture` returns that effective token — the viewer's `[PreferredCulture]`
if set, else `app.DefaultCulture`, else `""` (invariant) — as a value, so you can branch on it (`if (Session.CurrentCulture
== "de-DE") …`) or pass it on. It is the author-facing sibling of `Session.CurrentUser`, resolves the same way as the
implicit formatting above, and runs in the browser with no round trip.

**Parsing (the inverse).** `Convert.ToDecimal(s, "sv-SE")` reads a number written in a culture (`"1 234,56"` → `1234.56`),
returning `0` for a string it can't parse (the `Convert.To*` contract). It runs **client-side too**, byte-identical to
the server, over a defined profile: leading/trailing whitespace, one sign, group separators, and a decimal separator.
Outside that profile — a currency symbol in the string, parentheses for a negative, an exponent — parses server-side (a
round trip). A plain `Convert.ToDecimal(s)` (no culture) already parses invariantly in the browser.

`Convert.ToInt(s, "de-DE")` is the integer twin — it runs **client-side too**, over the same profile minus a decimal
point *and* minus a trailing sign (a `"1234-"` is a value for `ToDecimal` but `0` for `ToInt`). A value outside the
32-bit integer range parses to `0`, like every unparseable string.

**Parsing a date exactly.** `DateTime.ParseExact(s, "yyyy-MM-dd", "sv-SE")` reads a date/time written to a specific
pattern — the inverse of the date formatter. It is STRICT: a string that does not match the pattern *throws* (unlike the
number parses, which return `0`), exactly as .NET's `DateTime.ParseExact` does. It runs **client-side** for a literal
custom pattern that carries a full date — a 4-digit year (`yyyy`), a month, and a day-of-month — plus optional time,
using the supported tokens `y M d H h m s t` with separators and quoted literals; the field widths are strict (`MM`
wants two digits, `"2024-6-15"` is rejected), month/day names honour the culture (`MMMM`/`dddd`, genitive included), and
a day-name token must agree with the date. A **single-char standard specifier** (`"d"`, `"D"`), a **2-digit year**
(`yy` — its value depends on the culture's century window), or a **dynamic** pattern/culture stays server-side. The
lenient, multi-pattern `DateTime.Parse(s)` (no explicit pattern) also stays server-side.

## Examples       {#examples}

Format money and a percentage for an explicit culture:

```osy title="currency + percent" test app=stdlib-culture-formatting
string Price(decimal amount) {
  return amount.ToString("C", "sv-SE");     // "1 234,56 kr"
}

string Rate(decimal ratio) {
  return ratio.ToString("P1", "en-US");     // "12.5 %"
}
```

Format a date, standard and custom:

```osy title="dates" test app=stdlib-culture-formatting
string LongDate(DateTime when) {
  return when.ToString("D", "de-DE");        // "Freitag, 5. Januar 2024"
}

string Iso(DateTime when) {
  return when.ToString("yyyy-MM-dd", "sv-SE");
}
```

Parse a number a user typed in their culture (runs in the browser):

```osy title="culture parse" test app=stdlib-culture-formatting
decimal ReadPrice(string entered) {
  return Convert.ToDecimal(entered, "sv-SE");    // "1 234,56" → 1234.56, "" → 0
}

int ReadQuantity(string entered) {
  return Convert.ToInt(entered, "de-DE");        // "1.234" → 1234, "12,5" → 0 (no decimal point)
}

DateTime ReadDate(string entered) {
  return DateTime.ParseExact(entered, "dd/MM/yyyy", "en-GB");   // "25/12/2024"; a mismatch throws
}
```

Set an app-wide default so a culture-less format still localises:

```osy title="app default culture" test app=stdlib-culture-formatting
app.DefaultCulture = "sv-SE";

string LocalPrice(decimal amount) {
  return amount.ToString("C");               // uses sv-SE → "1 234,56 kr"
}
```

Let each viewer see prices in their own culture — mark a `Culture` property on the principal `[PreferredCulture]`:

```osy title="per-viewer culture" test app=stdlib-culture-formatting-viewer
app.DefaultCulture = "sv-SE";

[Principal] entity Member {
  [Required, MaxLength(60)] string Email;
  [PreferredCulture] Culture Locale;         // a BCP-47 token, e.g. "de-DE"
}

string ViewerPrice(decimal amount) {
  return amount.ToString("C");               // renders in the signed-in member's Locale, else sv-SE
}

string ViewerCulture() {
  return Session.CurrentCulture;             // the effective token: the member's Locale, else "sv-SE"
}
```

## See also       {#see-also}
- [DateTime](https://osysharp.com/reference/types/datetime/) — the date/time values these formats render
- [Encoding — Base64, URL, HTML](https://osysharp.com/reference/stdlib/encoding/) — the other C#-faithful text surfaces


---

<!-- https://osysharp.com/reference/stdlib/encoding/ -->

# Encoding — Base64, URL, HTML

> Encode and decode text — Base64, URL percent-encoding, and HTML escaping — with the C#-faithful spellings. `Convert.ToBase64String` / `FromBase64String` for binary, `Uri.EscapeDataString` / `UnescapeDataString` for URLs, `WebUtility.HtmlEncode` / `HtmlDecode` for HTML. All pure — no capability needed.

<!-- id: stdlib-encoding · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/encoding/ -->

## Summary        {#summary}
Three small encoding surfaces, spelled exactly as in C#, all **pure** (no capability, no `using` required):

```osy syntax
var b64  = Convert.ToBase64String(bytes);          // binary  → base64 text
var back = Convert.FromBase64String(b64);          // base64  → binary
var q    = Uri.EscapeDataString("a b&c");          // "a%20b%26c" — safe in a URL
var safe = WebUtility.HtmlEncode("<b>hi</b>");      // "&lt;b&gt;hi&lt;/b&gt;" — safe in HTML text
```

## Signature      {#signature}
```osy syntax
string  Convert.ToBase64String(byte[] bytes)     // Binary → base64 text
byte[]  Convert.FromBase64String(string s)       // base64 text → Binary
string  Uri.EscapeDataString(string s)           // percent-encode for a URL
string  Uri.UnescapeDataString(string s)         // reverse
string  WebUtility.HtmlEncode(string s)          // escape for HTML text
string  WebUtility.HtmlDecode(string s)          // reverse
```

## Description    {#description}
**Base64** — `Convert.ToBase64String` turns a `Binary` value (bytes, e.g. a file read via <span class="planned" title="this page is planned and not written yet">storage-file</span> or a
`Binary` property) into standard base64 text; `Convert.FromBase64String` reverses it. Use it to carry bytes inside a
JSON body or a text field.

**URL escaping** — `Uri.EscapeDataString` percent-encodes a string so it is safe inside a URL query value or path
segment (RFC 3986: a space becomes `%20`, `&` becomes `%26`, and so on). `Uri.UnescapeDataString` reverses it. Pair it
with [Http.*](https://osysharp.com/reference/http/facade/) when building a request URL from user input:

```osy syntax
var url = "https://api.example.com/search?q=" + Uri.EscapeDataString(term);
```

**HTML escaping** — `WebUtility.HtmlEncode` escapes `&`, `<`, `>`, `"` so a string is safe to place in HTML text
without injecting markup; `WebUtility.HtmlDecode` reverses it.

**Not yet available.** Raw UTF-8 byte conversion (`Encoding.UTF8.GetBytes` / `GetString`) is a planned follow-on —
Base64 already carries bytes as text, and JSON round-trips `byte[]` as base64 automatically. Ask for it when you need
raw UTF-8 bytes directly.

## Examples       {#examples}

Build a signed-looking token payload as base64:

```osy title="base64, round-tripped" test app=stdlib-encoding
string Encode(byte[] payload) {
  return Convert.ToBase64String(payload);
}

byte[] Decode(string base64) {
  return Convert.FromBase64String(base64);
}
```

Escape user input into an outbound request ([Http.*](https://osysharp.com/reference/http/facade/)):

```osy title="escaping user input into a URL" test app=stdlib-encoding
app Shop {
  model "model/**/*.osy";
  use Osysharp.Http;         // `use` is a MANIFEST declaration — it belongs in app.osy
}

string Search(string term) {
  var url = "https://api.example.com/search?q=" + Uri.EscapeDataString(term);
  var r = Http.Get(url);
  return r.Body;
}
```

Render user text safely into an HTML fragment:

```osy title="escaping user text into an HTML fragment" test app=stdlib-encoding
string Cell(string userText) {
  return "<td>" + WebUtility.HtmlEncode(userText) + "</td>";
}
```

## See also       {#see-also}
- [Uri](https://osysharp.com/reference/stdlib/uri/) — parse a URL into its parts (`new Uri(url).Host`)
- [Regex](https://osysharp.com/reference/stdlib/regex/) — pattern matching, replacing, and splitting text
- [Http.*](https://osysharp.com/reference/http/facade/) — the outbound HTTP surface these escapings feed


---

<!-- https://osysharp.com/reference/stdlib/guid/ -->

# Guid.* — the empty id, a fresh id, and reading one out of a string

> A `Guid` is the type an entity's `Id` has, and the type a reference to a row has. `Guid.NewGuid()` mints a fresh one, `Guid.Empty` is the all-zero value, and `Guid.Parse(s)` reads one out of text — a route parameter, a query string, a form field. `Parse` REFUSES text that is not a Guid rather than answering the all-zero id, and a `string` never converts to a `Guid` on its own: you write the call, exactly as in C#.

<!-- id: stdlib-guid · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/guid/ -->

## Summary        {#summary}

A route parameter arrives as a `string`. An entity's `Id` is a `Guid`. `Guid.Parse` is the call between them:

```osy title="a page that loads the row its route names" test app=stdlib-guid
entity Doc {
  [MaxLength(120)] string Title;
}

[Server, AllowAnonymous] Doc Fetch(Guid id) { return Doc.Single(d => d.Id == id); }

[Page("/doc/{id}"), AllowAnonymous]
component DocPage(string id) {
  render { Button("Load", onPress: Load); }
  action Load() { var doc = Fetch(Guid.Parse(id)); }
}
```

⚠ **You can often skip it entirely — declare the route parameter as a `Guid`** and the platform parses it for you
before your component ever runs. Reach for `Guid.Parse` when the text comes from somewhere the platform has not
already typed: a form field, a value you built, a string you were handed.

## Signature      {#signature}

```osy syntax
Guid Guid.Empty                 // the all-zero id — a static VALUE, written without parentheses
Guid Guid.NewGuid()             // a fresh random id
Guid Guid.Parse(string s)       // the string as a Guid — REFUSES text that is not one
```

## Description    {#description}

### `Guid.Parse` refuses, it does not coerce        {#parse-refuses}

`Guid.Parse("nope")` fails the call. It does **not** answer `Guid.Empty`, and that is the whole point: an all-zero id
silently substituted for a mistyped URL would be a confident lookup of a row that does not exist, which is a *wrong*
answer rather than a missing one. This is C#'s contract for `Parse`, and Osy# keeps it.

There is no `Convert.ToGuid`. The coercing `Convert.To*` family exists for the numeric types, where a blank form
field genuinely means zero; there is no value a Guid can fall back to that means "blank".

### Which id formats does `Guid.Parse` accept?        {#accepts}

Thirty-two hex digits, **with or without the four dashes**, in either letter case, with surrounding whitespace
trimmed. The answer is always the canonical lowercase dashed form, so a parsed id and one the server sent are the
same string:

```osy syntax
Guid.Parse("0f8fad5b-d9cb-469f-a165-70867728950e")   // ✅ the canonical form
Guid.Parse("0F8FAD5B-D9CB-469F-A165-70867728950E")   // ✅ → lowercased
Guid.Parse("0f8fad5bd9cb469fa16570867728950e")       // ✅ bare → comes back DASHED
Guid.Parse("  0f8fad5b-…-70867728950e  ")            // ✅ trimmed

Guid.Parse("{0f8fad5b-…-70867728950e}")              // ❌ braces are not accepted
Guid.Parse("(0f8fad5b-…-70867728950e)")              // ❌ nor parentheses
Guid.Parse("")                                       // ❌ empty is not Guid.Empty
```

⚠ **This is narrower than .NET's `Guid.Parse`, on purpose.** .NET also takes the brace, parenthesis and
`{0x…,0x…,{0x…}}` forms. Osy# runs your code on several engines — the server, the browser, and compiled JavaScript —
and the accepted set has to be *identical* on all of them or the same string parses in one place and fails in
another. A set that large cannot be matched honestly across all of them, so the contract is the part every engine
can meet exactly. Nothing the platform produces is ever in one of the refused forms.

### A `string` never becomes a `Guid` on its own        {#no-implicit-conversion}

```osy syntax
void Load(Guid id) { … }

Load(routeParam);              // ❌ refused — cannot pass 'string' to a parameter of type 'Guid'
Load(Guid.Parse(routeParam));  // ✅
```

The same holds in every position — a `return`, a field write, an array element, a class initializer — and in the
other direction too: a `Guid` where a `string` is wanted needs `.ToString()` or an interpolation (`$"{id}"`).

⚠ **A comparison inside a query is a different rule and still works.** `Doc.Single(d => d.Id == someString)` resolves
— the query engine compares the underlying values — so the shape a page is built from is unaffected.

### `Guid.TryParse` — write it as you would in C#        {#no-tryparse}

`Guid.TryParse(s, out var id)` compiles. Osy# has no `out` parameter of its own — a call here can suspend and resume
elsewhere, so no caller frame is guaranteed to still be waiting for a write-back — but the compiler recognises this
one shape and rewrites it into the lines it means, ahead of the statement that holds it:

```osy title="what the compiler writes for you" syntax
if (Guid.TryParse(text, out var id)) { … }

Guid? id = null;                                 // ← the rewrite
try { id = Guid.Parse(text); } catch { }
if (id != null) { … }
```

So `id` is a `Guid?`, in scope for the rest of the block exactly as C# scopes an `out var`.

⚠ **Two positions are refused, and each says so with the form that works there:** a loop CONDITION (the parse would
run once instead of per iteration — test inside the body and `break`) and a lambda body (it belongs to the element —
put it in a function and call that). `out` into a variable that ALREADY exists is also refused: C# writes
`default(T)` there on failure and this language has no spelling for the zero of every type.

Where the text should already be valid, `Guid.Parse` on its own is the honest call — let the failure be a failure.
Where a route parameter may be junk, declare it a `Guid` and let the platform reject the request before your code
runs.

### Where these run        {#execution-side}

All three run **in the browser** — no round trip. `Guid.NewGuid()` is deliberately non-deterministic and is memoized
across a durable resume, so a re-entered function does not mint a second id after handing out the first.

`Guid.Parse` has no SQL push-down: using it inside a query predicate over a database column is refused at compile
time rather than translated, because Postgres accepts text this contract does not.

## Examples       {#examples}

```osy title="minting an id, and reading one back" test app=stdlib-guid-round
entity Invite {
  Guid Token;
  [MaxLength(60)] string Label;
}

Invite Open(string label) {
  return new Invite { Token = Guid.NewGuid(), Label = label };
}

string Show(Guid token) { return token.ToString(); }

Invite Redeem(string token) {
  var t = Guid.Parse(token);
  return Invite.Single(i => i.Token == t);
}
```

## See also       {#see-also}

- [The standard library — and where each call runs](https://osysharp.com/reference/stdlib/index/) — the whole standard library, and where each call runs
- [component](https://osysharp.com/reference/ui/component/) — a routed component and its parameters, including typing one as a `Guid` so no parse is needed
- [entity members](https://osysharp.com/reference/entity/properties/) — the properties an entity has, `Id` included


---

<!-- https://osysharp.com/reference/stdlib/random/ -->

# Random.* — an ordinary random number

> A random integer for ordinary work — picking a row, jittering a retry, shuffling. `Random.Next(10)` answers 0..9. It WORKS INSIDE A LINQ QUERY and translates to SQL, so `.OrderBy(f => Random.Next())` is one round trip and `.ThenBy(f => Random.Next())` is a random tiebreak. It is SEEDABLE and therefore reproducible, which is why it must never produce a token: `Security.RandomId` is the unguessable one.

<!-- id: stdlib-random · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/random/ -->

## Summary        {#summary}

`Random.Next` answers a random integer, spelled exactly as C#'s `System.Random.Next`.

```osy syntax
int n = Random.Next(10);        // 0..9   — max is EXCLUSIVE, as in C#
int d = Random.Next(1, 7);      // 1..6   — min inclusive, max exclusive
```

There are two randoms in this platform and choosing between them is the whole of what you need to know:

| you want | use | why |
|---|---|---|
| a number, a pick, a shuffle, a jitter | **`Random.Next`** | ordinary, fast, and reproducible under a test seed |
| a token, a reset code, an api key, a session id | **`Security.RandomId`** / `RandomHex` | cryptographic — see [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/) |

## Signature        {#signature}

```osy syntax
int  Random.Next()                   // any non-negative int — the ordering form
int  Random.Next(int max)            // [0, max)   — `max` must be at least 1
int  Random.Next(int min, int max)   // [min, max) — `max` must be greater than `min`
```

An empty range has no value to answer with, so it is refused rather than answered: `Random.Next(0)` and
`Random.Next(5, 5)` both throw, naming the bound. `Random.Next(list.Count)` over an empty list is the usual way to
reach that, so check the count first.

## Description        {#description}

**It is seedable, and that is not an implementation detail.** A test that draws must be able to draw the same
sequence twice, or it can only assert statistically — "at least 25 of 40 were idle" — which is slow and flaky by
construction. Seeding is what makes an exact assertion possible.

**And that is exactly why it must not make a secret.** A value anyone can reproduce is not unguessable. The
sequence is predictable from any earlier value, and nothing about the result *looks* wrong: it is not obviously
sequential, the tests pass, and it stays guessable until somebody enumerates it. C# draws this same line between
`System.Random` and `RandomNumberGenerator`, and gets the same misuse.

`security-weak-random` is a MUST-tier lint on exactly that mistake — a `Random` draw landing in a member named for
a credential (`token`, `secret`, `password`, `apiKey`, `nonce`, `otp`, …). It fires on the DESTINATION, never on
the call, because `Random.Next` is correct nearly everywhere.

## It works inside a LINQ query        {#ordering}

**`Random.Next()` is translated to SQL, so it is usable anywhere in a query — not only in a function body.** There
is no separate `OrderByRandom()` verb to learn, and none is needed: it is an ordinary `OrderBy` over an ordinary
call, which is what a C# author writes, and it therefore composes with `ThenBy`, `Where`, `Take` and the rest
exactly as any other sort key does.

This is how you take one row at random without a `Count()` and a `Skip()`:

```osy title="one row at random, and a fair tiebreak among equals" syntax
// one row at random — ONE round trip
Film any = Film.OrderBy(f => Random.Next()).Take(1).FirstOrDefault();

// the shape most selection rules actually take: least-compared, RANDOM AMONG TIES
Film pick = Film.OrderBy(f => f.Comparisons)
                .ThenBy(f => Random.Next())
                .Take(1).FirstOrDefault();
```

The second is the one worth knowing. Without a random tiebreak, "the least-compared film" returns the *same* film
every time two are tied, so a matchmaker walks the same pair repeatedly. With it, the primary sort still decides and
the tie is broken fairly, in one query.

It runs in the DATABASE, not in the browser and not in memory — `ORDER BY random()` — so the whole draw is one
round trip whatever the table's size. A `.Where(…)` before it narrows the rows the database is choosing among, as
you would expect:

```osy title="narrow first, so the database draws only from the rows you meant" syntax
Film pick = Film.Where(f => !f.Watched)
                .OrderBy(f => Random.Next())
                .Take(1).FirstOrDefault();
```

⚠ **Not inside a `live var`.** A reactive read re-runs whenever its inputs change, and a random order returns a
different row each time — so the value flickers when something unrelated moves. Draw in an `action`, store what you
drew, and let the page read the stored row.

## Examples        {#examples}

```osy test app=random-pick
entity Card { [Required, MaxLength(40)] string Face;
  security { allow read, create when IsAnonymous || IsAuthenticated; } }

// One of a known set, chosen at random.
string DealOne() {
  var faces = ["clubs", "diamonds", "hearts", "spades"];
  return faces[Random.Next(faces.Count)];
}

// A retry jitter — spread out so a hundred clients do not return at the same instant.
int BackoffMillis(int attempt) { return 500 * attempt + Random.Next(250); }
```

## See also        {#see-also}

- [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/) — `RandomId` / `RandomHex`, the cryptographic pair, for anything a stranger must not guess.


---

<!-- https://osysharp.com/reference/stdlib/regex/ -->

# Regex

> Match, replace, split and CAPTURE with regular expressions — the C#-faithful System.Text.RegularExpressions spelling. `Regex.IsMatch(s, "\\d+")` tests a pattern, `Regex.Replace` rewrites every match, `Regex.Split` breaks a string on a pattern, and `Regex.Match`/`Regex.Matches` return match objects whose groups are read by number or by name. Pure — no capability needed. Every call runs under a platform match-timeout.

<!-- id: stdlib-regex · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/regex/ -->

## Summary        {#summary}
**`Regex`** is the regular-expression surface, spelled exactly as in C# (`System.Text.RegularExpressions`):

```osy syntax
var ok    = Regex.IsMatch("order-1042", "\\d+");          // true — the pattern matches somewhere
var clean = Regex.Replace("a1b2c", "\\d", "-");           // "a-b-c" — every match rewritten
var parts = Regex.Split("a, b ,c", "\\s*,\\s*");          // ["a", "b", "c"]
var m     = Regex.Match("ERROR:42", "(?<level>\\w+):(\\d+)");
//          m.Success · m.Value · m.Index · m.Groups["level"] · m.Groups[2]
```

It is **pure** — no capability, no `using` required (though a pasted `using System.Text.RegularExpressions;` is
accepted and does nothing). Patterns are standard .NET regex syntax.

## Signature      {#signature}
```osy syntax
bool         Regex.IsMatch(string input, string pattern)                       // matches anywhere?
string       Regex.Replace(string input, string pattern, string replacement)   // rewrite every match
List<string> Regex.Split(string input, string pattern)                         // split on the pattern
Match        Regex.Match(string input, string pattern)                         // the FIRST match
Match[]      Regex.Matches(string input, string pattern)                       // every match
```

## Description    {#description}
**`IsMatch`** returns true when `pattern` matches anywhere in `input`. Anchor with `^`…`$` for a whole-string match.

**`Replace`** returns `input` with every match rewritten to `replacement`. The replacement supports numbered group
backreferences — `$1`, `$2`, … — exactly as in C#:

```osy title="rewriting with numbered backreferences" syntax
Regex.Replace("2026-07-12", "(\\d+)-(\\d+)-(\\d+)", "$1/$2/$3")   // "2026/07/12"
```

**`Split`** returns the substrings between matches as a `List<string>` you can `foreach`, index, and read `.Count` on.

**Match timeout (host protection).** Every `Regex` call runs under a fixed platform match-timeout. A pattern with
*catastrophic backtracking* (a nested quantifier like `(a+)+$` on a long non-matching input) can otherwise spin a CPU
effectively forever; past the cap the call is aborted rather than allowed to hang. You cannot raise the cap — write a
cheaper pattern if you hit it. This is the only safety limit on the surface; normal patterns never come near it.

### Capturing groups — `Match` and `Matches`   {#groups}

**`Match`** returns the FIRST match; **`Matches`** returns every one, left to right and non-overlapping.

`Match` **always returns an object.** An unsuccessful search answers a `Match` with `Success = false` rather than
null — so `if (m.Success)` is the question you ask, and there is nothing to null-check first.

```osy title="an unsuccessful Match is still an object, not null" syntax
var m = Regex.Match(line, "(?<level>\\w+):(\\d+)");
m.Success        // did it match?
m.Value          // the whole matched text
m.Index          // where it starts, in characters
m.Length         // how long it is
m.Groups.Count   // how many groups, counting the whole match at 0
```

A group is read **by number or by name**, and a group that did not participate answers `Success = false` with an
empty `Value` — never a fault, and never a missing entry:

```osy title="reading a group by number or by name — a missing one is not a fault" syntax
m.Groups[0]          // the WHOLE match — group numbering starts at 0, exactly as in C#
m.Groups[2].Value    // a numbered group
m.Groups["level"]    // a named group, from `(?<level>…)`
m.Groups[99]         // Success = false — out of range is a group that did not participate
m.Groups["nope"]     // Success = false — so an OPTIONAL capture reads without a prior check
```

⚠ **Named groups are numbered AFTER unnamed ones**, which is .NET's rule and not source order. In
`(?<level>\\w+):(\\d+)` the `level` group is number **2** and `(\\d+)` is number **1**. Read a named group by its
name and the question does not arise.

⚠ **`Match` and `Matches` do not push down into a query.** Postgres' regex dialect is POSIX rather than .NET's, and
`regexp_matches` returns a set of arrays rather than a scalar — so a pushed-down match would be a different function
wearing the same name, and the compiler refuses it where you wrote it. Narrow the query with `Regex.IsMatch`, which
DOES push down, and capture from the rows it returns.

⚠ **They run on the SERVER only.** `IsMatch`, `Replace` and `Split` also run in the browser; `Match`/`Matches` do
not yet, and a render expression that calls one is a compile error naming the component. Call them in a function or
an action and render what they produce.

**Still not available.** The compiled-instance form (`new Regex(pattern)`) — the three statics plus the two match
members cover validation, rewriting, tokenizing and extraction.

## Examples       {#examples}

Validate an email field:

```osy title="validate a whole string — anchor it, or it matches anywhere" test app=stdlib-regex
bool LooksLikeEmail(string s) {
  return Regex.IsMatch(s, "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
}
```

Pull the parts out of a log line — by name, so the group NUMBERS never have to be counted:

```osy title="capture by NAME, so group numbers never have to be counted" test app=stdlib-regex-capture
string Level(string line) {
  var m = Regex.Match(line, "(?<level>[A-Za-z]+):(?<code>[0-9]+)");
  return m.Success ? m.Groups["level"].Value : "";
}

/// Every code in a line, in order — `Matches` is an ordinary array, so `foreach` reads it.
string[] Codes(string line) {
  var out = new List<string>();
  foreach (var m in Regex.Matches(line, "(?<level>[A-Za-z]+):(?<code>[0-9]+)")) {
    out.Add(m.Groups["code"].Value);
  }
  return out.ToArray();
}
```

Normalise whitespace and strip punctuation:

```osy title="rewrite every match — punctuation into a slug" test app=stdlib-regex
string Slugify(string title) {
  var lower = Text.Lower(Text.Trim(title));
  var spaced = Regex.Replace(lower, "[^a-z0-9]+", "-");   // non-alphanumerics → a single dash
  return spaced;
}
```

Tokenise a delimited line, tolerating irregular spacing:

```osy title="split on a pattern, tolerating irregular spacing" test app=stdlib-regex
List<string> Fields(string csvLine) {
  return Regex.Split(csvLine, "\\s*,\\s*");               // "a, b ,c" → ["a", "b", "c"]
}
```

## See also       {#see-also}
- <span class="planned" title="this page is planned and not written yet">stdlib-text</span> — the plain-string operations (`Upper`, `Contains`, `Replace`, `Split`) for non-pattern work
- [JsonSerializer](https://osysharp.com/reference/json/serializer/) — the other pure BCL surface for parsing structured text


---

<!-- https://osysharp.com/reference/stdlib/security/ -->

# Security.* — hashing, verifying, tickets, random ids

> The calls an authentication flow needs: `HashPassword` (salted, one-way), `VerifyPassword` (constant-work comparison against a stored hash, and a one-argument form for the no-such-account path), `IssueJwt` (the session ticket a login returns), and `RandomId`/`RandomHex` for unguessable tokens. They are the building blocks of an `[AuthMethod]` login/signup — you never store a plaintext password, and you never compare hashes yourself.

<!-- id: stdlib-security · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/security/ -->

## Summary        {#summary}
`Security.*` is what an authentication function is built from — hash a password on the way in, verify it on the way
back, and hand out a ticket:

```osy title="the three that make a login" test app=stdlib-security
[AuthMethod]
string Login(string email, string password) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  // No such account. Verify against nothing anyway — it costs the same as a real check, so the clock does not
  // tell a stranger which addresses are registered. See "the no-account path" below.
  if (u == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); }
  return "";
}

[AuthMethod]
string Signup(string email, string password) {
  var u = new User { Email = email, PasswordHash = Security.HashPassword(password) };
  return Security.IssueJwt(u.Id, u.Email);
}
```

## Signature      {#signature}
```osy syntax
string Security.HashPassword(string plain)                   // salted one-way hash — store THIS, never the password
bool Security.VerifyPassword(string plain, string hash)     // does the plaintext match the stored hash?
bool Security.VerifyPassword(string plain)                        // no account to check — spend the time, answer false
string Security.IssueJwt(Guid userId, string email)         // the session ticket a login/signup returns

string Security.RandomId()                                  // an unguessable id, default length
string Security.RandomId(int length)                        // …of a given length
string Security.RandomHex(int length)                       // random hex characters
```

## Description    {#description}

### How do I hash a password, and check one?   {#passwords}
`HashPassword` produces a **salted, one-way hash**. The salt is generated for you and travels inside the returned
value, so two users with the same password get different hashes — and the same password hashed twice is never the
same value. That has a consequence worth stating, because it surprises people: **you cannot compare hashes**.

```osy title="✗ two hashes of one password never match — verify instead" syntax
if (u.PasswordHash == Security.HashPassword(password)) { … }   // ❌ never true. Not "insecure" — WRONG.
if (Security.VerifyPassword(password, u.PasswordHash)) { … }   // ✅ the only way to check a password
```

A hash is an ordinary `string`, and the column you store it in is an ordinary bounded one:

```osy title="a hash is an ordinary bounded string column" syntax
[MaxLength(200)] string PasswordHash;      // the column a hash goes in
```

⚠ **Nothing about the TYPE stops you comparing hashes** — both sides of the ❌ line above are strings, so it compiles
and is simply always false. What catches it is `osy lint`, which reports it as `security-password-compared-directly`
at MUST tier. The guarantee that the hash never leaves is carried entirely by the field mask below, not by the type.

`VerifyPassword` takes the **plaintext first, the stored hash second** — the order matters, and swapping them fails
every login. It re-derives the hash with the salt it finds in the stored value and compares them safely.

#### The no-account path — `VerifyPassword(plain)`   {#no-account}

A login that returns the moment no row matches is giving a **correct answer with the wrong timing.** Hashing is
deliberately slow — that is what a password KDF is for — so an unknown address answers in microseconds where a known
one takes the full work factor. The response body is identical and the clock is not, so anyone can feed a list of
addresses to a page that is *meant* to be public and learn which of them hold accounts. That is a disclosure on its
own (who banks here, who uses this clinic) and it is the first half of every credential-stuffing run.

The one-argument form is the fix, and it is a security primitive rather than a convenience — it verifies against
nothing, pays the same KDF, and answers `false`:

```osy syntax
var u = User.Where(x => x.Email == email).FirstOrDefault();
if (u == null) { return ""; }                                  // ❌ answers in microseconds — an enumeration oracle
if (u == null) { Security.VerifyPassword(password); return ""; }   // ✅ costs what a real check costs
```

`osy lint` reports the ❌ shape as `security-login-enumerates-users`, at MUST tier.

⚑ `VerifyPassword(password, "")` does the same thing — an empty stored hash still costs a full verify, deliberately,
so that a row with no credential (an OAuth-only account, a truncated column) cannot answer faster than a wrong
password either. Prefer the one-argument form: it says "there is nothing to check" rather than leaving the reader to
work out what an empty second argument means.

Store only the hash. The plaintext password should exist nowhere in your app: not in a column, not in a log, not on a
second entity "for the reset flow". `HashPassword` at the point of signup is the whole story, and the
[field mask](https://osysharp.com/reference/security/entity-security/) (`deny read PasswordHash when !IsAuthenticator`) is how you make sure the hash
itself is never read by anything but the login.

### How do I mint a session ticket? — `IssueJwt`   {#issuejwt}
`Security.IssueJwt(userId, email)` mints the **session ticket** — a signed token, scoped to your app and to that user.
Return it from your `[AuthMethod]` `Login`/`Signup`, and the login page hands it to `Session.SignIn(ticket)`, which
stores it as the session bearer: the next request arrives authenticated as that user, carrying whatever roles they
have been granted.

Two rules follow from what the ticket *is*:

- **Issue it only after you have verified the credential.** `IssueJwt` does not check anything — it signs whatever
  user you name. It is the *conclusion* of a login, never a step in one.
- **Return `""` for a failed sign-in**, not a ticket and not an exception. `Session.SignIn("")` stores nothing and the
  visitor stays anonymous. Returning the same empty answer whether the email is unknown or the password is wrong is
  also what stops `Login` from telling a stranger which addresses have accounts.

A ticket carries the grants the user has **at the moment the next request is served** — it is a claim of identity, not
a frozen snapshot of permissions. Grant a role during signup (see [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/)) and it is in force
immediately.

### How do I make an unguessable token? — `RandomId` / `RandomHex`   {#random}
Cryptographically random strings, for the things that must be **unguessable**: a password-reset token, an invite code,
an API key, a one-time link. `RandomId()` takes a default length, `RandomId(length)` a chosen one, and
`RandomHex(length)` gives you hex characters.

```osy title="an api key nobody can guess" test app=stdlib-security
string IssueApiKey(User owner) {
  var key = Security.RandomId(32);
  new ApiKey { Owner = owner, Token = key };
  // Handed back HERE, once, to the caller who asked for it. The row itself never gives it up again — see the
  // `deny read Token` on `ApiKey` below. That is how the platform's own keys work too: `osy user apikey generate`
  // prints the `pk_…` once and stores only what it needs to check it.
  return key;
}
```

Do not reach for these for a database key: an entity's `Id` is already a unique identifier. Reach for them when the
value's job is to be **secret**.

⚠ **A secret whose recipient is NOT the caller — a reset token, an invite code, a one-time link — is only half done
when you have minted it.** Its whole purpose is to travel out of band and come back, so the app has to be able to
send it: a [`client { }`](https://osysharp.com/reference/http/client/) block, a secret for the provider, and a call. Mint it and stop, and the
step that later asks for it can be satisfied by nobody, however green the tests are — `osy lint` reports that as
`security-reset-token-never-delivered` (MUST). [[security-auth-bootstrap#examples]] shows a password reset with its
delivery wired, and `demo/auth-demo` is the same flow end to end with tests.

⛔ **And never hand such a token back to the caller who asked for it.** This example returned
`Security.RandomId(32)` from an `[AuthMethod] string StartReset(string email)` until 2026-09-04 — so anyone who
knew your address could ask for your reset token and be given it, which is precisely the account takeover the
out-of-band channel exists to prevent. The value goes to the mailbox; the caller learns nothing either way.

### Has this secret been given a value? — `IsSecretSet`   {#issecretset}
A declared secret has no value until someone sets one (`osy secret set <NAME>`), and reading an unset secret fails
at the point of use — which is usually deep inside the call that needed it. `Security.IsSecretSet("NAME")` answers
whether it has one, so a feature that depends on a secret can say so plainly instead:

```osy title="degrade with a message, rather than failing where the secret is read" syntax
string Summarise(string body) {
  if (!Security.IsSecretSet("OPENAI")) { return "Summaries are off — no OPENAI key is configured."; }
  return Llm.Complete(body);
}
```

It answers whether a value EXISTS, never what it is — there is no verb that reads a secret out into your code.

## Examples       {#examples}
The declarations the examples above are written against — the credential entity, the roles, and the wiring that makes
`Login`/`Signup` reachable while signed out:

```osy title="the app these functions live in" test app=stdlib-security
[Role] enum AppRole { Authenticator, Member }

[Principal]
entity User {
  // [Unique] or two rows share a login, and which account a password opens is then undefined — a race no
  // check-then-insert in application code can close, because the race is in the database.
  [Required, Unique, MaxLength(255)] string Email;
  [MaxLength(200)] string PasswordHash;          // the HASH — the password itself is stored nowhere
  security {
    allow read   when IsAuthenticator;
    allow create when IsAuthenticator;
    allow read where Id == user.Id;
    deny read PasswordHash when !IsAuthenticator; // and only the auth flow ever reads even the hash
  }
}

entity RoleGrant {
  [Required] User User;
  [Required] AppRole Role = AppRole.Member;
}

entity ApiKey {
  [Required] User Owner;
  [MaxLength(64)] string Token = "";
  security {
    allow read, create where Owner == user;   // the rows are yours; the SECRET on them is nobody's
    // ⛔ A ROW GRANT IS NOT A FIELD GRANT, and "only the owner can read it" is not a reason to skip this. A
    //    readable stored secret is readable forever, by every later page, export and audit that touches the row.
    //    `IssueApiKey` hands the value to its caller once; after that there is nothing left to read.
    deny read Token;
  }
}

policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);

app.AuthBootstrap = new AuthBootstrap {
  Role          = AppRole.Authenticator,
  Login         = Login,
  Signup        = Signup,
};
```

## See also       {#see-also}
- [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/) — the `[AuthMethod]` marker these functions carry
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — the identity they run as, and what it is allowed to touch
- [app.Auth — how the platform authenticates a user of your app](https://osysharp.com/reference/security/password-auth/) — `app.Auth`, where the platform does the hashing and ticket-issuing for you
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — granting a role at signup without opening a path to self-elevation


---

<!-- https://osysharp.com/reference/stdlib/index/ -->

# The standard library — and where each call runs

> The pure standard library, and the one fact about it that changes how your app feels: 126 of its 155 methods run in the BROWSER, with no round trip. The rest that do not are held there by ONE thing — they need a secret the browser must never hold (a signing key, a KMS key, a password hash). Nothing here needs a capability.

<!-- id: stdlib-index · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/index/ -->

## Summary        {#summary}
The standard library is the set of **pure** calls available in any function body — text, numbers, dates, encoding,
hashing. No `use`, no capability, no ceremony.

The thing worth knowing about it is **where each call runs**. Osy# decides that for you: a function whose body the
browser can execute runs *in the browser*, and never makes a network call. **126 of the 155 stdlib methods can.** So

```osy syntax
var label = Text.Upper(name);              // runs in the browser
var price = Convert.ToString(total, "F2"); // runs in the browser
var due   = Date.AddDays(start, 30);       // runs in the browser
```

…are all free. You do not opt in, and there is no annotation. Write the code; the compiler works out where it can
run.

## Where each call runs        {#description}
**The default is the client, and the goal is that it always is.** A method is kept on the server only when running it in
the browser would give a *different or unsafe answer* — never because it was hard to port. There is one reason that is a
true floor, and a shrinking frontier of methods on their way to the client.

### 1 — It needs a secret the browser must never hold        {#host-authority}
This is the floor — a handful of `Security.*` and `Crypto.*` operations, and the only ones that can *never* move. The
client is the **user's own machine**, so the JWT signing key, the KMS encryption key, an HMAC key and a password
hash's cost/salt are precisely the things it must not see. Running these in the browser would not be slow — it would
either **leak the secret** or let the client **forge authority** (a client-minted token is self-issued). The round
trip is the security boundary, not a performance cost.

```osy title="the handful that can never leave the server" syntax
Security.HashPassword(pw)     // server — the cost factor and the salt RNG are the host's
Security.IssueJwt(claims)     // server — signed with the host's key; a client could forge one
Crypto.Encrypt(secret)        // server — the KMS key never reaches the browser
```

Host-authoritative randomness sits here too: `Security.RandomId` / `RandomHex` must come from the host's RNG, or an
"unguessable" id is guessable.

The clock is **not** one of these, and it is worth saying so directly because it looks like it should be. `DurableClock.Now`,
`DurableClock.UtcNow` and `DateTime.UtcNow` **run on the client.** A `DateTime` here is a UTC *instant*, not a wall-clock
reading, so a browser and the server name the same value — 9am in Frankfurt *is* 5pm in Tokyo, the same instant — and
reading it in the browser costs no round trip. (Test pinning still works, and a resumed durable function still sees a
consistent instant, because this engine resumes from a saved point rather than re-running your function from the top.)

```osy title="the clock is NOT one of them — it reads in the browser" syntax
var now  = DurableClock.Now;                        // client — a UTC instant, read in the browser
var due  = Date.AddDays(order.Placed, 30);   // client — pure arithmetic on a value in hand
var year = Date.Year(order.Placed);          // client
```

### 2 — The browser cannot yet reproduce it faithfully        {#fidelity}
This has shrunk to essentially one case, and it is a specific fidelity gap, not a law of nature: a `Double` handed to
`Convert.ToString(value, format)` still renders on the server, because .NET produces its digits from the value's exact
*binary* expansion. (`Decimal` — this platform's money type, and what a format specifier is almost always for — already
formats in the browser.)

Everything that used to sit here has moved to the client: `WebUtility.HtmlDecode` now ports .NET's *own* algorithm and
entity table (not the browser's, which decodes a different set — it accepts `&copy` *without* the semicolon where .NET
does not); `WebUtility.HtmlEncode` ships its five-character escape set; and the unkeyed hashes `Crypto.Sha256Hex` /
`Crypto.Md5Hex` run in the browser too, since neither holds a secret.

## Which two calls decide per call site?        {#conditional}
Two methods are not client-or-server by *name* — the **arguments** decide, so the same method may run in the browser at
one call site and on the server at the next.

**`Convert.ToString(value, format)`** — and the `$"{total:F2}"` that lowers onto it. It runs in the browser when the
format is a *literal* — either a *standard* specifier (`F2`, `N0`, `C`, `P1`) or a *custom* digit pattern
(`"#,##0.00"`, `"0.00"`, `"00000"`) — over a `Decimal` or `Int32`. A format built at *runtime*, or a `Double`, still
goes to the server.

```osy syntax
var a = Convert.ToString(total, "F2");        // client — literal, standard, decimal
var b = Convert.ToString(total, "#,##0.00");  // client — the compiler parses the pattern into a plan and ships it
var c = Convert.ToString(total, fmt);         // server — a runtime format string the compiler cannot see
```

A *literal* custom pattern (`b`) is a compile-time constant, so the compiler parses it **once** into a plan — how many
digits, whether to group — and ships the plan; the browser executes arithmetic, never a parser (the same trick regular
expressions already use). A pattern that reaches past the digit-placeholder core — section separators, scaling,
percent, scientific, embedded literals — stays on the server, where .NET renders it correctly.

**A v1 boundary worth stating plainly:** client-side custom-pattern formatting works only for a **compile-time literal**
pattern, because the mechanism is the *compiler* parsing it into a plan. A **runtime-built** format string (`c` — where
the format is a variable) the compiler cannot see, so it stays on the server; today translating it would mean shipping
a second .NET-format parser to every browser. That is a v1 limitation, not a permanent one: a future version could ship
a small client-side format *interpreter* so a runtime-built pattern also runs in the browser. Until then, `c` is a
round trip and returns the right string.

**`JsonSerializer.Serialize(value)`** runs in the browser for a scalar or a list of scalars. Hand it a `class` or an
entity and it goes to the server: naming and ordering the members requires the model, which the browser does not hold.
Guessing at a member order would produce JSON that parses and is wrong.

## Does this change my code?        {#does-it-change-my-code}
**No.** Where a call runs never changes its *answer* — the client arms are pinned against the real server by an oracle
suite, down to the details C# gets opinionated about (`Math.Round(2.5)` is `3`, away from zero, on both sides;
`Uri.EscapeDataString` escapes `!'()*`, which `encodeURIComponent` does not).

It changes only how the app *feels*. A form that formats a price, validates a pattern and computes a due date now does
all of it without touching the network. And if you write a function that mixes a client-runnable call with a
server-only one, the function simply runs on the server — the answer is still right, it just costs a round trip.

Here is the surface actually running — and the two answers C# has an opinion about, pinned. The client arms are held
to exactly these, so the assertions below are true **wherever the function ends up running**:

```osy title="a function that never touches the network" test app=stdlib-index
// Every call in this body is client-runnable, so the whole function runs in the browser. No annotation, no opt-in.
string PriceLabel(decimal total) {
  return Text.Upper("total") + ": " + Convert.ToString(total, "F2");
}
```

```osy title="the same answer on either side" run app=stdlib-index
[Test]
void The_stdlib_gives_the_same_answer_wherever_it_runs() {
  Assert.Equal("TOTAL: 12.50", PriceLabel(12.5m));           // two decimals, not "12.5"
  Assert.Equal("ACME", Text.Upper("acme"));

  // AWAY FROM ZERO, not banker's rounding — `Math.Round(2.5)` is 3. A client that used JS's Math.round
  // would agree here and disagree on 3.5, which is how a half-cent walks into an invoice.
  Assert.Equal(3m, Math.Round(2.5m));
  Assert.Equal(4m, Math.Round(3.5m));

  // A literal custom pattern — the money-grid case — runs in the browser too: the compiler parses "#,##0.00"
  // into a plan and ships it. Grouped, two decimals, rounded away from zero on the exact digits.
  Assert.Equal("1,234.57", Convert.ToString(1234.5678m, "#,##0.00"));

  // The deterministic date surface: pure arithmetic on a value already in hand. Runs in the browser.
  var due = Date.AddDays(DateTime.Parse("2026-01-01"), 30);
  Assert.Equal(31, Date.Day(due));
  Assert.Equal(2026, Date.Year(due));
}
```

## Every method, and where it runs        {#the-table}
Generated from the compiler's own allowlist and **checked on every build** — if a method moves, is added, or is
removed, this table fails the build until it is right. It cannot go stale.

**103 run on the client · 9 are server-only · 2 decide per call site.**

#### `Clock`

| Method | Runs on | Why not the client |
|---|---|---|
| `Backoff.Cap` | server | bounds a retry policy, which only the durable engine reads |
| `Backoff.Exponential` | server | a retry policy is read only by the durable engine, which schedules the retry — a client holds nothing that acts on one |
| `Backoff.Fixed` | server | a retry policy is read only by the durable engine, which schedules the retry — a client holds nothing that acts on one |
| `Backoff.Jitter` | server | bounds a retry policy, which only the durable engine reads |
| `Backoff.Linear` | server | a retry policy is read only by the durable engine, which schedules the retry — a client holds nothing that acts on one |
| `Backoff.MaxAttempts` | server | bounds a retry policy, which only the durable engine reads |
| `Convert.AmbientAppField` | client | ships as an appField node — the client-safe app identity (Slug / Domain / BaseUrl) from the boot app ambient (the internal carrier of `App.Slug`/`Domain`/`BaseUrl`) |
| `Convert.AmbientCulture` | client | ships as an ambientCulture / cultureFormatAmbient node — the viewer's culture token from the metadata payload (the internal carrier of Session.CurrentCulture) |
| `Convert.AmbientUserField` | client | ships as a currentUserField node — a scalar of the viewer's principal from the boot bag (the internal carrier of `Session.CurrentUser` fields) |
| `Convert.FromBase64String` | client | — |
| `Convert.ToBase64String` | client | — |
| `Convert.ToBool` | client | — |
| `Convert.ToDecimal/1` | client | — |
| `Convert.ToDecimal/2` | conditional | gated per call site — a literal declared culture ships as a cultureParse node; a dynamic culture stays server |
| `Convert.ToDouble` | client | — |
| `Convert.ToInt/1` | client | — |
| `Convert.ToInt/2` | conditional | gated per call site — a literal declared culture ships as a cultureParse (int profile) node; a dynamic culture stays server |
| `Convert.ToInt64` | client | — |
| `Convert.ToString/1` | client | — |
| `Convert.ToString/2` | conditional | gated per call site — a literal standard specifier or a plannable custom pattern over a Decimal/Int32 ships; a runtime format, a Double, or an unplannable pattern stays server |
| `Convert.ToString/3` | conditional | gated per call site — a literal supported format + a literal (declared) culture over a Decimal/Int32 ships as a cultureFormat node keyed by token; anything else stays server |
| `Crypto.Decrypt` | server | decryption uses the app's platform-managed KMS key, which the client never holds |
| `Crypto.Encrypt` | server | encryption uses the app's platform-managed KMS key, which the client never holds |
| `Crypto.FixedTimeEquals` | server | a constant-time compare is only meaningful next to the secret it guards |
| `Crypto.HmacSha256Hex` | server | an HMAC is keyed; the key is the host's and must not reach the browser |
| `Crypto.Md5Hex` | client | — |
| `Crypto.Sha256Hex` | client | — |
| `Date.AddDays` | client | — |
| `Date.AddHours` | client | — |
| `Date.AddMinutes` | client | — |
| `Date.AddMonths` | client | — |
| `Date.AddYears` | client | — |
| `Date.Date` | client | — |
| `Date.Day` | client | — |
| `Date.DayOfWeek` | client | — |
| `Date.Hour` | client | — |
| `Date.Minute` | client | — |
| `Date.Month` | client | — |
| `Date.Second` | client | — |
| `Date.Year` | client | — |
| `DateOnly.FromDateTime` | client | — |
| `DateOnly.New` | client | — |
| `DateOnly.Parse` | client | — |
| `DateTime.MaxValue` | client | — |
| `DateTime.MinValue` | client | — |
| `DateTime.New` | client | — |
| `DateTime.Parse` | client | — |
| `DateTime.ParseExact` | conditional | gated per call site — a literal custom yyyy-pattern + a literal declared culture ships as a cultureDateParse node; a standard specifier / 2-digit year / dynamic pattern\|culture stays server |
| `DateTime.Today` | client | — |
| `DateTime.UtcNow` | client | — |
| `DateTimeOffset.UtcNow` | client | — |
| `DurableClock.Now` | client | — |
| `DurableClock.Today` | client | — |
| `DurableClock.UtcNow` | client | — |
| `Enum.Description` | client | model-backed; ships as std:Enum.Description, dispatched by the client's dedicated arm |
| `Enum.Label` | client | model-backed; ships as std:Enum.Label, dispatched by the client's dedicated arm over the enum map |
| `Enum.Name` | client | model-backed; ships as std:Enum.Name, dispatched by the client's dedicated arm |
| `Enumerable.Range` | client | — |
| `File.SignedUrl` | server | needs the session's app + the signing key to mint a signed URL — never leaves the server |
| `File.Url` | client | ships as fileUrl — the render evaluator builds the URL in-process |
| `Guid.Empty` | client | — |
| `Guid.NewGuid` | client | — |
| `Guid.Parse` | client | — |
| `JsonSerializer.Serialize` | conditional | gated by CanJsonSerializeOnClient — a scalar or list of scalars ships; a class or entity needs the server's model |
| `Math.Abs` | client | — |
| `Math.Acos` | client | — |
| `Math.Asin` | client | — |
| `Math.Atan` | client | — |
| `Math.Atan2` | client | takes **(y, x)** — C#'s order. `Atan(y / x)` is not the same: it loses the sign and folds two quadrants onto two others |
| `Math.Ceil` | client | — |
| `Math.Ceiling` | client | — |
| `Math.Clamp` | client | — |
| `Math.Cos` | client | radians |
| `Math.Exp` | client | — |
| `Math.Floor` | client | — |
| `Math.Log` | client | — |
| `Math.Log10` | client | — |
| `Math.Log2` | client | — |
| `Math.Max` | client | — |
| `Math.Min` | client | — |
| `Math.Pow` | client | — |
| `Math.Round` | client | — |
| `Math.Sign` | client | — |
| `Math.Sin` | client | radians |
| `Math.Sqrt` | client | — |
| `Math.Tan` | client | radians |
| `Math.Truncate` | client | — |
| `Regex.IsMatch` | client | ships as regexIsMatch, with the pattern translated to JS's dialect at compile time |
| `Regex.Replace` | client | ships as regexReplace, with the pattern translated to JS's dialect at compile time |
| `Regex.Split` | client | ships as regexSplit, with the pattern translated to JS's dialect at compile time |
| `Security.HashPassword` | server | password hashing runs where the host's cost factor and salt RNG live |
| `Security.IssueJwt` | server | a JWT is signed with the host's key; a client-minted token would be self-issued authority |
| `Security.LinkOAuthFromPending` | server | it opens a server-sealed pending-oauth token and writes the identity link; the verified subject never crosses to the client |
| `Security.RandomHex` | server | an unguessable value must come from the host's RNG, not the user's machine |
| `Security.RandomId` | server | an unguessable id must come from the host's RNG, not the user's machine |
| `Security.VerifyPassword` | server | password verification runs where the hash does — the client never sees a hash |
| `Security.VerifyPendingOAuthEmail` | server | it opens a server-sealed pending-oauth token; the callback-verified email re-enters only inside the seal, never as a client argument |
| `Text.ByteSize` | client | — |
| `Text.Capitalize` | client | — |
| `Text.Concat` | client | — |
| `Text.Contains` | client | — |
| `Text.EndsWith` | client | — |
| `Text.IndexOf` | client | — |
| `Text.IsBlank` | client | — |
| `Text.IsEmpty` | client | — |
| `Text.Join` | client | — |
| `Text.LastIndexOf` | client | — |
| `Text.Left` | client | — |
| `Text.Length` | client | — |
| `Text.Like` | client | — |
| `Text.Lower` | client | — |
| `Text.PadEnd` | client | — |
| `Text.PadStart` | client | — |
| `Text.Repeat` | client | — |
| `Text.Replace` | client | — |
| `Text.Reverse` | client | — |
| `Text.Right` | client | — |
| `Text.Split` | client | — |
| `Text.StartsWith` | client | — |
| `Text.Substring` | client | — |
| `Text.TitleCase` | client | — |
| `Text.Trim` | client | — |
| `Text.TrimEnd` | client | — |
| `Text.TrimStart` | client | — |
| `Text.Truncate` | client | — |
| `Text.Upper` | client | — |
| `TimeOnly.FromDateTime` | client | — |
| `TimeOnly.New` | client | — |
| `TimeOnly.Parse` | client | — |
| `TimeSpan.Days` | client | — |
| `TimeSpan.FromDays` | client | — |
| `TimeSpan.FromHours` | client | — |
| `TimeSpan.FromMilliseconds` | client | — |
| `TimeSpan.FromMinutes` | client | — |
| `TimeSpan.FromSeconds` | client | — |
| `TimeSpan.Hours` | client | — |
| `TimeSpan.Minutes` | client | — |
| `TimeSpan.New` | client | — |
| `TimeSpan.Parse` | client | — |
| `TimeSpan.Seconds` | client | — |
| `TimeSpan.TotalDays` | client | — |
| `TimeSpan.TotalHours` | client | — |
| `TimeSpan.TotalMilliseconds` | client | — |
| `TimeSpan.TotalMinutes` | client | — |
| `TimeSpan.TotalSeconds` | client | — |
| `TimeSpan.Zero` | client | — |
| `Uri.EscapeDataString` | client | — |
| `Uri.UnescapeDataString` | client | — |
| `WebUtility.HtmlDecode` | client | — |
| `WebUtility.HtmlEncode` | client | — |
| `Zone.InZone` | conditional | ships for a LITERAL declared zone (client has its DST plan); a dynamic zone → server |
| `Zone.IsDst` | conditional | ships for a LITERAL declared zone; a dynamic zone → server |
| `Zone.New` | server | the zone factory is a compile-time literal fold — no runtime work to accelerate |
| `Zone.OffsetAt` | conditional | ships for a LITERAL declared zone; a dynamic zone → server |
| `Zone.Resolve` | conditional | gated per call site — a literal (declared) zone ships as a zoneResolve node; a dynamic zone stays server |
| `string.Concat` | client | — |
| `string.Join` | client | — |

## See also        {#see-also}
- <span class="planned" title="this page is planned and not written yet">stdlib-text</span> · [Regex](https://osysharp.com/reference/stdlib/regex/) · [Encoding — Base64, URL, HTML](https://osysharp.com/reference/stdlib/encoding/) · [Uri](https://osysharp.com/reference/stdlib/uri/) — the individual surfaces
- [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/) — the server-only authority surface, and why it is


---

<!-- https://osysharp.com/reference/stdlib/zones/ -->

# Time zones — the Zone type and its operations

> Store and use civil time zones. `Zone` is a value-kind holding an IANA id (`"Europe/Stockholm"`); `zone.OffsetAt`, `zone.IsDst`, `instant.InZone(zone)` and `zone.Resolve(date, time)` are DST-aware and run in the browser for a declared zone; `zone.Resolve` turns a civil rule ("opens 09:00 local") into a UTC instant.

<!-- id: stdlib-zones · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/zones/ -->

## Summary        {#summary}
A **`Zone`** is a value-kind that stores a civil time zone by its **IANA id** (`"Europe/Stockholm"`, `"Asia/Kolkata"`).
You store it on an entity like any scalar, and its operations are **DST-aware**:

```osy title="storing a zone on an entity, as its IANA id" syntax
entity Shop {
  Zone OpensInZone;      // stored as the IANA token, e.g. "Europe/Stockholm"
  TimeOnly OpenTime;
}
```

```osy title="the DST-aware operations on a zone" syntax
var z      = Zone.Of("Europe/Stockholm");
var offset = z.OffsetAt(instant);       // the UTC offset AT that instant — +1h in winter, +2h in summer
var summer = z.IsDst(instant);          // is daylight-saving in effect then?
var local  = instant.InZone(z);         // show a UTC instant as local wall-clock time
var opensUtc = z.Resolve(today, opens); // "09:00 in the zone" → the UTC instant
```

For a **declared** zone named as a literal, `OffsetAt` / `IsDst` / `InZone` / `Resolve` run **in the browser**,
byte-identical to the server — no round trip.

## Signature      {#signature}
```osy syntax
Zone      Zone(string ianaId)                 // the factory — the id must be a declared zone
TimeSpan  zone.OffsetAt(DateTime instant)     // total UTC offset at an instant (DST-aware, sub-hour exact)
bool      zone.IsDst(DateTime instant)        // is daylight-saving in effect at an instant
DateTime  instant.InZone(Zone zone)           // a UTC instant as the zone's local wall-clock time (for display)
DateTime  zone.Resolve(DateOnly date, TimeOnly timeOfDay)   // a civil local time → the UTC instant
```

## Description    {#description}
**Declared, closed set.** A zone id written as a literal (`Zone.Of("Europe/Stockholm")`) must belong to the app's declared
`zones { }` set. The platform ships a default pack (`UTC`, `Europe/London`, `Europe/Stockholm`, `Europe/Berlin`,
`Europe/Paris`, `Europe/Dublin`, `America/New_York`, `America/Chicago`, `America/Denver`, `America/Los_Angeles`,
`America/Sao_Paulo`, `Asia/Kolkata`, `Asia/Kathmandu`, `Asia/Tokyo`, `Asia/Shanghai`, `Australia/Sydney`); an app can
vendor the block to curate it. A literal id outside the set is a **compile error** (with a "did you mean"). The declared
zones are also browsable data (an `Osysharp.Locale.Zone` table) so a picker can list them.

**Stored value is the token.** A `Zone` field stores the IANA string itself, not a foreign key — portable and stable.
A value written at runtime is validated to be a real IANA zone; a garbage token is rejected.

**The underlying methods.** The instance spellings above are the idiomatic form of the `Zone` stdlib module:
`Zone.New` (the factory), `Zone.OffsetAt`, `Zone.IsDst`, `Zone.InZone` and `Zone.Resolve`. For a literal declared zone,
`Zone.OffsetAt` / `Zone.IsDst` / `Zone.InZone` / `Zone.Resolve` all run in the browser (`Zone.New` folds at compile
time). A **dynamic** zone — a stored `Zone` field or a variable — has no bundled client plan, so its operations run on
the server (where the answer is identical); side-inference routes them automatically.

**DST-aware, and correct for hard zones.** Offsets come from the pinned time-zone data, so sub-hour zones
(`Asia/Kolkata` +05:30, `Asia/Kathmandu` +05:45) and negative-DST zones (`Europe/Dublin`) are exact — not just
whole-hour Western zones.

**`Resolve` — the civil-time rule.** `zone.Resolve(date, timeOfDay)` answers "what UTC instant is it when the wall clock
in this zone reads *date* at *timeOfDay*?" — the DST-aware way to store "the shop opens 09:00 local". At the twice-a-year
edges it is deterministic: an **ambiguous** local time (the fall-back hour that happens twice) resolves to the standard
offset; an **invalid** local time (the spring-forward hour that never happens) is skipped forward past the gap. Real
business hours never fall in that 02:00–03:00 window.

## Examples       {#examples}

Show a stored UTC timestamp in a fixed zone, and read its offset:

```osy title="offset + display" test app=stdlib-zones
DateTime InStockholm(DateTime instant) {
  return instant.InZone(Zone.Of("Europe/Stockholm"));   // 12:00 UTC → 13:00 (winter) / 14:00 (summer)
}

TimeSpan StockholmOffset(DateTime instant) {
  return Zone.Of("Europe/Stockholm").OffsetAt(instant); // +01:00 in winter, +02:00 in summer
}
```

The civil-time rule — "the shop opens 09:00 in its own zone" → a UTC instant:

```osy title="civil rule (stored zone)" test app=stdlib-zones
entity Shop {
  [Required, MaxLength(120)] string Name;
  Zone OpensInZone;
  TimeOnly OpenTime;
}

DateTime OpeningUtc(Shop shop, DateOnly on) {
  return shop.OpensInZone.Resolve(on, shop.OpenTime);
}
```

The same rule with a **literal** zone runs in the browser (no round trip):

```osy title="civil rule (literal zone)" test app=stdlib-zones
DateTime StockholmOpening(DateOnly on, TimeOnly at) {
  return Zone.Of("Europe/Stockholm").Resolve(on, at);   // 09:00 civil → the UTC instant, DST-aware
}
```

Is daylight-saving in effect right now for a zone?

```osy title="is-dst" test app=stdlib-zones
bool SummerTime(Zone zone) {
  return zone.IsDst(DateTime.UtcNow);
}
```

## See also       {#see-also}
- [DateTime](https://osysharp.com/reference/types/datetime/) — the `DateTime` / `DateOnly` / `TimeOnly` values these operations take and return
- [Culture formatting — ToString(format, culture)](https://osysharp.com/reference/stdlib/culture-formatting/) — format the resulting local time for a culture


---

<!-- https://osysharp.com/reference/stdlib/uri/ -->

# Uri

> Parse a URL into its parts with the C#-faithful `new Uri(url)` handle. Construct it from a URL string, then read `.Scheme`, `.Host`, `.Port`, `.AbsolutePath`, `.PathAndQuery`, `.Query`, and `.Fragment`. Pure — no capability needed. Pairs with the HTTP surface (parse a webhook or redirect URL).

<!-- id: stdlib-uri · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/uri/ -->

## Summary        {#summary}
**`Uri`** is the parsed-URL handle, spelled exactly as in C#. Construct it from a URL string and read its parts:

```osy syntax
var u = new Uri("https://api.example.com:8443/v1/users?active=true#top");
u.Scheme;         // "https"
u.Host;           // "api.example.com"
u.Port;           // 8443
u.AbsolutePath;   // "/v1/users"
u.PathAndQuery;   // "/v1/users?active=true"
u.Query;          // "?active=true"
u.Fragment;       // "#top"
```

It is **pure** — no capability, no `using` required. `Uri` is also where the URL-escaping helpers live
([Encoding — Base64, URL, HTML](https://osysharp.com/reference/stdlib/encoding/) — `Uri.EscapeDataString`), exactly as C#'s `System.Uri` carries both.

## Signature      {#signature}
```osy syntax
Uri     new Uri(string url)      // parse an ABSOLUTE URL

string  uri.Scheme               // "https"
string  uri.Host                 // "api.example.com"
int     uri.Port                 // 8443 (or the scheme default: 443 for https, 80 for http)
string  uri.AbsolutePath         // "/v1/users"
string  uri.PathAndQuery         // "/v1/users?active=true"
string  uri.Query                // "?active=true" (empty when there is none)
string  uri.Fragment             // "#top" (empty when there is none)
```

## Description    {#description}
`new Uri(url)` parses an **absolute** URL once and exposes its components as instance members — the same shape as C#'s
`System.Uri`. When the URL omits the port, `.Port` is the scheme's default (443 for `https`, 80 for `http`). `.Query`
and `.Fragment` include their leading `?` / `#`, and are empty strings when absent.

An **invalid** or **relative** URL throws — `new Uri` parses an absolute URI, faithful to C#. Validate untrusted input
first (e.g. with [Regex](https://osysharp.com/reference/stdlib/regex/)) if a throw is not what you want.

## Examples       {#examples}

Route on the host of a webhook URL, parsing an [Http.*](https://osysharp.com/reference/http/facade/) target:

```osy title="check the host before trusting a webhook URL" test app=stdlib-uri
bool IsTrustedHost(string webhookUrl) {
  var u = new Uri(webhookUrl);
  return u.Scheme == "https" && Text.EndsWith(u.Host, ".example.com");
}
```

Build a URL with an escaped query value ([Encoding — Base64, URL, HTML](https://osysharp.com/reference/stdlib/encoding/)) and read it back:

```osy title="build a URL with an escaped query value" test app=stdlib-uri
Uri Search(string term) {
  return new Uri("https://api.example.com/search?q=" + Uri.EscapeDataString(term));
}
```

## See also       {#see-also}
- [Encoding — Base64, URL, HTML](https://osysharp.com/reference/stdlib/encoding/) — `Uri.EscapeDataString` / `UnescapeDataString` for building the URL you parse
- [Http.*](https://osysharp.com/reference/http/facade/) — the outbound HTTP surface whose URLs this parses
- [Regex](https://osysharp.com/reference/stdlib/regex/) — validate a URL string before parsing untrusted input


---

<!-- https://osysharp.com/reference/stdlib/escaping/ -->

# Uri and WebUtility escaping

> Percent-encodes a string for a URL, and escapes a string for safe insertion into HTML. Escaping is exact and identical in the browser and on the server — including the characters that a browser's own encodeURIComponent would leave alone.

<!-- id: stdlib-escaping · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/escaping/ -->

## Summary        {#summary}
`Uri.EscapeDataString` percent-encodes a string so it can be dropped safely into a URL. `WebUtility.HtmlEncode`
escapes a string so it can be dropped safely into HTML text. Both have inverses,
`Uri.UnescapeDataString` and `WebUtility.HtmlDecode`.

## Signature      {#signature}
```osy syntax
Uri.EscapeDataString(<string> s) -> string
Uri.UnescapeDataString(<string> s) -> string
WebUtility.HtmlEncode(<string> s) -> string
WebUtility.HtmlDecode(<string> s) -> string
```

## Description    {#description}

### URL escaping   {#url}

`Uri.EscapeDataString` keeps only the RFC 3986 *unreserved* characters — letters, digits, and `- . _ ~` — and
percent-encodes everything else as UTF-8 bytes:

```osy title="URL escaping — unreserved kept, everything else percent-encoded" syntax
Uri.EscapeDataString("hello world")   // "hello%20world"
Uri.EscapeDataString("a&b=c")         // "a%26b%3Dc"
Uri.EscapeDataString("café")          // "caf%C3%A9"
```

That includes `!`, `'`, `(`, `)` and `*`, which some URL encoders leave alone. Escaping the same string always
produces the same output, wherever the code runs.

`Uri.UnescapeDataString` reverses it, and is **forgiving**: a malformed escape is left exactly as written rather than
raising. `Uri.UnescapeDataString("%zz")` is `"%zz"`, and a `+` is a literal plus, not a space.

### HTML escaping   {#html}

`WebUtility.HtmlEncode` escapes the five characters that can break out of HTML text — `"`, `&`, `'`, `<`, `>` — and
renders every non-ASCII character as a numeric entity:

```osy title="HTML escaping — the five characters that break out of text" syntax
WebUtility.HtmlEncode("<script>alert('x')</script>")
// "&lt;script&gt;alert(&#39;x&#39;)&lt;/script&gt;"
```

Note that it does **not** escape `+`, `/`, `?` or `#` — they are harmless in HTML text. It escapes for **text**, not
for an attribute value or a URL; do not use it to build a `href`, and do not use it as a substitute for the platform's
own output escaping, which already applies wherever a value is rendered.

## Examples       {#examples}
```osy title="build a search link" test app=text-search
string SearchUrl(string term) {
  return "/search?q=" + Uri.EscapeDataString(term);
}
// SearchUrl("blue & green")  ->  "/search?q=blue%20%26%20green"
```

## See also       {#see-also}
- [Regex](https://osysharp.com/reference/stdlib/regex/) — pattern matching, which is likewise identical on both sides
- [execution side](https://osysharp.com/reference/function/execution-side/) — why escaping runs in the browser, with no round trip


---

<!-- https://osysharp.com/reference/storage/file-versions/ -->

# A file's history — supersede, and what each version keeps

> Replacing a stored file's content while keeping what it said before. The FILE is the identity — a record pointing at it never re-points, and its grants keep covering the whole history — while each content it has had is a version you can list, show and download. Superseding copies nothing.

<!-- id: storage-file-versions · area: storage · stability: preview · html: https://osysharp.com/reference/storage/file-versions/ -->

## Summary        {#summary}

A `FileAsset` is a file's **identity**; a `FileVersion` is one **content** it has had. The asset points at the version
that is current, so:

- a business record's FK never re-points — `doc.Pdf` is the same row before and after a revision
- the file's grants cover its whole history, because a version has no authorization of its own
- superseding **copies nothing** — a new content is a new row and a moved pointer

```osy syntax
var pdf = File.Create("decision.pdf", "application/pdf", bytes);   // the file, and its version 1
File.Supersede(pdf, corrected);                                    // version 2; version 1 is untouched
```

## Signature      {#signature}

```osy syntax
FileAsset File.Create(string name, string mimeType, byte[] bytes)
void      File.Supersede(FileAsset asset, byte[] bytes)
```

## Description    {#description}

### Why a file is two things    {#two-things}

Because the two change on different clocks. A document's *identity* — what a case links to, who may see it, which
folder it is filed in — outlives any particular content, and a revision must not disturb it. Its *content* is what
gets replaced, and what has to be kept.

Writing content on the file itself would mean copying the old bytes somewhere on every revision. Writing a chain of
files instead would mean every record pointing at one had to follow the chain to find out what "current" is. Neither
is what an app wants to write.

### What each version keeps    {#what-a-version-keeps}

| | |
|---|---|
| `Version` | 1 for the first content, counting up |
| `CreatedBy` / `CreatedAt` | **who** replaced it, and **when** — the platform's own audit stamps |
| `Name` / `MimeType` | what the file was called and what type it was **at the time**, so a rename is history too |
| `Size` / `ContentHash` | derived from the bytes by the platform, never declarable by an app |
| the content | so any version can be shown or downloaded, not just the current one |

⚠ **A version's `CreatedAt` is when that content STOPPED being current**, not when it started — the row is written at
the moment it is replaced. A history that wants "in force from X to Y" takes Y from the version and X from the
previous one (or from the file's own `CreatedAt`, for version 1).

### Showing every version a file has had    {#listing}

`asset.Versions` is every content the file has had, current one included — so a history list is one loop with no
special last entry.

```osy syntax
foreach (var v in doc.Pdf.Versions.OrderBy(v => v.Version)) {
  Row(gap: Space.Gutter) {
    Text("v" + v.Version);
    Text(v.Name);
    Text(Text.ByteSize(v.Size));
    if (v.Id == doc.Pdf.CurrentVersion.Id) { Badge("current", tone: Tone.Success); }
  }
}
```

### Downloading one version    {#downloading}

A signed URL names **one content**. Ask for a version and the link fetches that version and nothing else — not the
next one, not the current one. Whoever may read the file may read all of it, but a link handed to a browser is a
bearer capability, and the one beside "version 2" has no business also fetching version 7.

### Erasing a file    {#erasing}

Deleting a `FileAsset` deletes its versions **and their bytes**. That is not a convenience: a history that outlived
its file would be a retention problem wearing an archive costume, and rows removed while their content stayed in the
store would make "erase this person's files" true only on paper.

⚠ **The consequence to accept: the only way to remove one version is to remove the file.** For an archive that is
correct — selective deletion of history is what an archive exists to prevent.

## Traps that cost real time   {#traps}

**A file's size lives on its content.** `asset.Size` does not exist; `asset.CurrentVersion.Size` is what it says now,
and `v.Size` is what a given version weighs. Every one of them counts against the app's storage quota — a document
superseded fifty times is fifty stored contents.

**Superseding does not rename.** `Supersede` replaces bytes; `asset.Name` is an ordinary field you set yourself. The
version records the name that was in force, so a rename after the fact is not retroactive.

**A file can exist with no content.** `new FileAsset { Name = … }` is legal and gives you an identity with no
`CurrentVersion` — useful for a placeholder, and the reason a viewer should handle "nothing to show" rather than
assume bytes.

## Examples       {#examples}

```osy title="a case document, revised — and the link to it never moves" test app=storage-file-versions
app CaseFiles { use Osysharp.Storage; }

using Osysharp.Storage;

entity CaseDocument {
  [MaxLength(200)] string Title;
  FileAsset Pdf;
}

// A new document: `File.Create` writes the file AND its first content, along with the size, the hash and the
// inline-vs-external routing — none of which an app declares.
CaseDocument NewDocument(string title, UploadedFile f) {
  var doc = new CaseDocument { Title = title, Pdf = File.Create(f.FileName, f.ContentType, File.ReadAllBytes(f.Path)) };
  UnitOfWork.Commit();
  return doc;
}

// A revision. `doc.Pdf` is the SAME row afterwards, so nothing that points at this document has to be told —
// and what the document used to say is still there, under its own version number.
void ReplaceContent(CaseDocument doc, UploadedFile f) {
  File.Supersede(doc.Pdf, File.ReadAllBytes(f.Path));
  UnitOfWork.Commit();
}
```

## See also       {#see-also}

- [File.SignedUrl](https://osysharp.com/reference/storage/file-signed-url/) — how a private file reaches a browser at all
- [The PDF kit — a document viewer and a page thumbnail](https://osysharp.com/reference/ui/pdf-kit/) — showing a document, and one page of it as a picture
- [use](https://osysharp.com/reference/types/use/) — `use Osysharp.Storage;`, which puts all of this in scope


---

<!-- https://osysharp.com/reference/storage/file-signed-url/ -->

# File.SignedUrl

> Mints a temporary, signed URL that lets an authorized browser fetch a PRIVATE app file — one stored outside `public/` — without carrying a login token. `File.Url` serves only public files; `File.SignedUrl` is how a private report, invoice, or per-user image reaches the browser. The link expires on its own after a few minutes.

<!-- id: storage-file-signed-url · area: storage · stability: preview · html: https://osysharp.com/reference/storage/file-signed-url/ -->

## Summary        {#summary}
**`File.SignedUrl(path)`** returns a URL a browser can use to fetch a **private** app file — a file stored under any
path *other* than `public/`. The URL carries a short-lived, cryptographically signed token, so it works for anyone who
holds the link until it expires, and 404s otherwise. Use it for a per-user report, an invoice PDF, or a private image:

```osy syntax
string url = File.SignedUrl("reports/" + user.Id + "/q3.pdf");
```

Like the rest of the `File.*` surface it is gated on **`using Osysharp.Storage;`**.

## Signature      {#signature}
```osy syntax
using Osysharp.Storage;

string File.SignedUrl(string path)
```

## Description    {#description}
The app-file serving route treats `public/` and everything else very differently. A file under `public/` is served to
**anyone**, anonymously, and its URL is just [File.Url](https://osysharp.com/reference/storage/file-url/). A file under any other path is **private**: the
serving route refuses it outright — until the request carries a valid signed token. `File.SignedUrl` mints that token.

### Can I put it straight in a render argument?        {#server-side}
This is the key difference from [File.Url](https://osysharp.com/reference/storage/file-url/). `File.Url` is pure string formatting, so it can sit directly in a
render argument (`Image(src: File.Url(...))`) and be built by the browser. `File.SignedUrl` **signs with your app's
key**, which the browser never holds — so it runs on the server, inside a function or action. Call it where you have
the path in hand (an action that prepares a download, a function that returns a link) and hand the result to the UI.

### Who can use the link, and for how long?        {#temporary-capability}
A signed URL is a **bearer link**: whoever holds it can fetch that one file until it expires. That is exactly what lets
a browser `<img>` or a download load without a login header. Two properties keep it safe:

- **It expires.** The link is valid for a short window (currently 15 minutes), then stops working. Mint it when the
  page or download is requested, not far in advance.
- **It unlocks exactly one file, for one app.** The path and your application are sealed inside the token, so a link
  minted for `reports/a.pdf` cannot be edited to fetch `reports/b.pdf`, and a link from one app is meaningless on
  another. A tampered or expired link simply 404s — a private file's existence is never revealed to someone without a
  valid link.

You never see, choose, or store a key — the platform mints, protects, and rotates it, exactly as for
[Crypto.Encrypt and Crypto.Decrypt](https://osysharp.com/reference/function/crypto-encrypt/).

`File.SignedUrl` is signing, not a storage read: it does **not** check that the file exists (a missing file 404s when
the browser follows the link), and it is not available inside a query.

## Examples       {#examples}

An action returns a private, expiring download link for the caller's own report:

```osy title="a private, expiring download link" test app=storage-file-signed-url
using Osysharp.Storage;

[Principal] entity User { [MaxLength(200)] string Email; }

// The report was written earlier under a NON-public path, so it is not servable anonymously.
string ReportDownloadUrl(User user) {
  return File.SignedUrl("reports/" + user.Id + "/q3.pdf");
}
```

Writing a private file, then handing back a link to it:

```osy title="write a private file, then hand back a link" test app=storage-file-signed-url
using Osysharp.Storage;

string SaveAndLinkInvoice(string invoiceId, byte[] pdf) {
  var path = "invoices/" + invoiceId + ".pdf";   // NOT under public/ — private by default
  File.WriteAllBytes(path, pdf);
  return File.SignedUrl(path);                    // a temporary link the browser can open
}
```

## See also       {#see-also}
- [File.Url](https://osysharp.com/reference/storage/file-url/) — the public counterpart: a plain, permanent URL for a `public/` file
- [Crypto.Encrypt and Crypto.Decrypt](https://osysharp.com/reference/function/crypto-encrypt/) — the same "the platform owns your key" model, for encrypting stored values


---

<!-- https://osysharp.com/reference/storage/file-url/ -->

# File.Url

> Turns an app-relative file path into the public URL a browser can fetch it from — `File.Url("public/x.png")` returns `/_osy/files/public/x.png`. Pure sugar over the serving route, so it works directly inside a render argument like `Image(src: File.Url(item.ImagePath))`.

<!-- id: storage-file-url · area: storage · stability: preview · html: https://osysharp.com/reference/storage/file-url/ -->

## Summary        {#summary}
**`File.Url(path)`** builds the URL a browser uses to fetch an app file — it prepends the app-file serving route to an
app-relative path. `File.Url("public/products/latte.png")` returns `/_osy/files/public/products/latte.png`.

It is **pure** (it touches no storage — it just formats a string), which is what lets it appear directly in a render
argument, unlike the `File.*` read/write effects. The canonical use is a photo on a page:

```osy syntax
Image(src: File.Url(item.ImagePath), alt: item.Name);
```

Like the rest of the `File.*` surface it is gated on **`using Osysharp.Storage;`** — an app that hasn't opted into
storage gets a compile error, not a silently-built URL.

## Signature      {#signature}
```osy syntax
using Osysharp.Storage;

string File.Url(string path)
```

## Description    {#description}
`File.Url` is the display half of the file story. You write a file to the app-file store under a **`public/`** path
(with a `File.WriteAllBytes("public/…", bytes)` effect, or via an upload), keep that path on a record
(`CatalogItem.ImagePath = "public/products/latte.png"`), and render it with `Image(src: File.Url(item.ImagePath))`.

Two things make it safe on a **public, anonymous** page:

- **Only `public/` files are servable anonymously.** A browser `<img>` sends no auth, so the serving endpoint returns
  only files written under `public/` (every other path is a 404). Build your URLs from `public/` paths.
- **It's a pure value, evaluated where it's needed.** In a `[Render(CSR)]` page the URL is built on the client as the
  image renders; in a server-rendered page it's built on the server. Either way there's no round-trip and no data
  read — it's string formatting.

`File.Url` does not check that the file *exists* — it only formats the path. A missing file simply 404s when the
browser fetches it, exactly like any other broken image URL.

## Examples       {#examples}

A public product catalog — each item's stored `public/` path becomes an `<img src>`:

```osy title="a public catalog served straight from the public folder" test app=storage-file-url
using Osysharp.Storage;

entity CatalogItem {
  string Name;
  string ImagePath;              // e.g. "public/products/latte.png"
  security { allow read when IsAuthenticated || IsAnonymous; }        // anonymous read — a public catalog
}

[Page("/catalog")]
[AllowAnonymous]
[Render(CSR)]
component CatalogPage() {
  var items = CatalogItem.ToList();      // bound to a field, so the render has an answer to "when again?"
  render {
    Stack {
      foreach (var item in items) {
        Image(src: File.Url(item.ImagePath), alt: item.Name);
      }
    }
  }
}
```

Writing the file first (an admin action), then saving its path on the record:

```osy title="write the file, then store its path" test app=storage-file-url
using Osysharp.Storage;

string SavePhoto(string sku, byte[] photo) {
  var path = "public/products/" + sku + ".png";
  File.WriteAllBytes(path, photo);
  return path;                    // store this on CatalogItem.ImagePath
}
```

## See also       {#see-also}
- [[Searchable]](https://osysharp.com/reference/memory/searchable/) — the other capability-gated surface (`using Osysharp.Memory;` — searchable text)
- [style props](https://osysharp.com/reference/ui/styling/) — styling the image and its container


---

<!-- https://osysharp.com/reference/storage/index/ -->

# Files (addressing something the app stores)

> The platform stores a file two ways, and which one you hold decides how you address it. A PATH-keyed file lives at an address you chose and becomes a URL with `File.Url` (public) or `File.SignedUrl` (a time-limited grant to one caller). A `FileAsset` is a ROW with its own security and NO path at all — neither of those verbs can address one; you put it on a page with `Image(fileAsset: row.Photo.Id)` and the platform works the address out.

<!-- id: storage-index · area: storage · stability: stable · html: https://osysharp.com/reference/storage/index/ -->

## Summary        {#summary}
**There are two stores, and the first thing to know is which one you are holding.**

| what you have | how it is addressed | shown with |
|---|---|---|
| a **path** — `"public/logo.png"`, an [upload](https://osysharp.com/reference/ui/upload/) result | `File.Url(path)` · `File.SignedUrl(path)` | `Image(src: File.Url(path))` |
| a **`FileAsset`** — a row, from `File.Create` or `Image.Thumbnail`, or a `FileAsset` field | it has **no path**; the platform addresses it | `Image(fileAsset: row.Photo.Id)` — see [Showing a picture on a page](https://osysharp.com/reference/ui/image/) |

⚠ The verbs on this page take a **path string**. A `FileAsset` carries none, so `File.Url(asset)` and
`File.SignedUrl(asset)` do not exist and the compiler refuses them — that is the commonest first-day mistake, and
[Showing a picture on a page](https://osysharp.com/reference/ui/image/) is what you wanted.

**Within the path store, a stored file is addressed by URL, and which URL you ask for is an access decision.**

```osy title="public, and not public" test app=storage-index
using Osysharp.Storage;

[Principal] entity User { [MaxLength(200)] string Email; }

entity Item { [Required, MaxLength(200)] string ImagePath; }

// Anyone may fetch this — it is an asset, served to whoever asks.
string PublicImage(Item item) { return File.Url(item.ImagePath); }

// This one belongs to somebody, so the URL carries its own expiring grant.
string PrivateReport(User user) { return File.SignedUrl("reports/" + user.Id + "/q3.pdf"); }
```

## Description    {#description}
**[File.Url](https://osysharp.com/reference/storage/file-url/) is pure sugar over the serving route** — an app-relative path becomes `/_osy/files/…`, which
means it works directly inside a render argument. It grants nothing: what it addresses is served to whoever asks,
so it is right for assets and wrong for anything a person owns.

**[File.SignedUrl](https://osysharp.com/reference/storage/file-signed-url/) is the other case**, and it is the one to reach for by default when the file belongs
to somebody: the URL carries its own time-limited grant, so sharing it is a decision with an expiry rather than a
permanent one.

## See also   {#see-also}
- [Showing a picture on a page](https://osysharp.com/reference/ui/image/) — putting a picture on a page, from either store
- [File.Url](https://osysharp.com/reference/storage/file-url/) — the public address of a PATH, and where it is safe
- [File.SignedUrl](https://osysharp.com/reference/storage/file-signed-url/) — a time-limited grant to one caller, over a PATH
- [Image.Thumbnail, Resize and Convert (a stored image, transformed)](https://osysharp.com/reference/storage/images/) — a stored image transformed: `Image.Thumbnail`, `Resize`, `Convert`, each a new `FileAsset`


---

<!-- https://osysharp.com/reference/storage/images/ -->

# Image.Thumbnail, Resize and Convert (a stored image, transformed)

> Three server-side transforms over a stored image, each answering a NEW `FileAsset`: `Thumbnail` fits the image within `max` pixels on its longest side (aspect kept, JPEG), `Resize` makes it exactly `w`×`h` in its own format, `Convert` re-encodes the same pixels to another format. Nothing is generated on upload; you make a variant when you ask for one, and it is stored and quota-counted like any other file. Show one with `Image(fileAsset: variant.Id)`.

<!-- id: storage-images · area: storage · stability: preview · html: https://osysharp.com/reference/storage/images/ -->

## Summary        {#summary}
`Image.*` is a standard-library surface, like `Http.*`, that a capability gates: `using Osysharp.Images;` is what makes
it resolvable, and the capability declares no table of its own because every op **consumes** a `FileAsset` and
**produces** one. The result is a new file — grantable, counted against the app's storage quota, and deduplicated by
content, so asking twice for the same thumbnail stores it once.

**A `FileAsset` is a ROW and carries no path**, so [File.Url](https://osysharp.com/reference/storage/file-url/) and [File.SignedUrl](https://osysharp.com/reference/storage/file-signed-url/) — whose
argument is an app-relative *path* — cannot address one. To put a variant on a page, give the element the row:
`Image(fileAsset: thumb.Id)`. See [Showing a picture on a page](https://osysharp.com/reference/ui/image/).

Nothing runs at upload time. A photograph, a chosen file (see [upload](https://osysharp.com/reference/ui/upload/)) or a capture (see [camera and microphone](https://osysharp.com/reference/ui/capture/)) is
stored as it arrived; the variant exists the moment your code asks for it, in the same unit of work.

## Signature      {#signature}
```osy syntax
using Osysharp.Images;      // gates the surface; the results are Osysharp.Storage.FileAsset, so that capability is in play too

FileAsset small = Image.Thumbnail(source, 128);          // fit within 128 px on the longest side, aspect kept, JPEG
FileAsset exact = Image.Resize(source, 800, 600);        // exactly 800×600, in the source's own format
FileAsset png   = Image.Convert(source, "image/png");    // the same pixels, re-encoded
```

## Description    {#description}

### The three ops   {#ops}

| Op | Answers | Format |
|---|---|---|
| `Image.Thumbnail(source, max)` | the image scaled to fit within `max` px on its longest side, aspect preserved | JPEG |
| `Image.Resize(source, w, h)` | the image at exactly `w`×`h` | the source's |
| `Image.Convert(source, mime)` | the same pixels | the `mime` you name |

Each is a call that leaves the function's own process — a server-side transform — so inside a workflow it is its
own durable step, the same as any other outside effect.

### What can go wrong   {#failures}
The source must be a stored image with bytes: a missing asset, an empty one, or a file that is not a decodable
image **throws**. The new bytes count against the app's storage quota; when they would exceed it the write is
refused with the storage budget's own exception, exactly as an upload would be. Both are ordinary exceptions to
catch where the page has something sensible to show instead.

### The thumbnail on a list   {#example}
The shape every app with an image on a row ends up writing — one function, called when the row is drawn:

```osy title="a thumbnail for a receipt, made on demand and stored once" test app=storage-images
using Osysharp.Storage;
using Osysharp.Images;

entity Receipt {
  [Required, MaxLength(140)] string Merchant;
  FileAsset? Photo;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

FileAsset Thumbnail(FileAsset source) {
  return Image.Thumbnail(source, 128);
}
```

`demo/file-manager` is the same one-liner in a whole app (`model/functions.osy`). What it puts on the page is
`Image(fileAsset: f.Id, alt: f.Name, w: 30)` — a row is a row, whether it is the original or a variant, and the
asset's own security decides who is served the bytes: a file that belongs to one person is refused to everyone else
without the page doing anything about it.

## Examples       {#examples}

Two variants of one upload, both stored, both addressed by their own id:

```osy title="two variants of one upload, each stored under its own id" syntax
FileAsset thumb = Image.Thumbnail(item.Photo, 128);   // in a function — an Image.* op is an effect
FileAsset web   = Image.Resize(item.Photo, 1200, 800);
```

…and each is shown by its own row, never by a URL the app builds:

```osy title="how a variant reaches a page — by its row" syntax
Image(fileAsset: item.Thumb.Id, alt: item.Merchant, w: 128)
```

## See also       {#see-also}
- [Showing a picture on a page](https://osysharp.com/reference/ui/image/) — how a variant actually reaches a page: `Image(fileAsset: …)`
- [Files (addressing something the app stores)](https://osysharp.com/reference/storage/index/) — the two ways a file is stored, and which address each one has
- [File.SignedUrl](https://osysharp.com/reference/storage/file-signed-url/) — a time-limited grant to one caller, over a PATH (not over a `FileAsset`)
- [upload](https://osysharp.com/reference/ui/upload/) — where the `FileAsset` a transform reads usually comes from
- [camera and microphone](https://osysharp.com/reference/ui/capture/) — a photograph from the camera, arriving as the same `UploadedFile`


---

<!-- https://osysharp.com/reference/testing/assert/ -->

# Assert

> The assertions a test makes. Beyond the usual equality and null checks there are comparisons (Greater, Less, InRange), a regex check (Matches), collection checks (NotEmpty, Count), a general predicate (That) — and the two that earn their keep: Assert.Throws proves a rule is enforced and Assert.Denied proves security is.

<!-- id: testing-assert · area: testing · stability: stable · html: https://osysharp.com/reference/testing/assert/ -->

## Summary        {#summary}
`Assert.*` is what a test claims. The everyday ones are equality and null checks; the two that earn their keep are
**`Assert.Throws`** (a rule really is enforced) and **`Assert.Denied`** (a principal really cannot do it).

## Signature      {#signature}
```osy syntax
Assert.Equal(<expected>, <actual>)      Assert.NotEqual(<a>, <b>)
Assert.Equal(<expected>, <actual>, <precision>)                             // equal to N decimal places
Assert.True(<bool>)                     Assert.False(<bool>)
Assert.Null(<value>)                    Assert.NotNull(<value>)
Assert.Contains(<needle>, <haystack>)   Assert.StartsWith(<prefix>, <text>)
Assert.Greater(<a>, <b>)                Assert.Less(<a>, <b>)               // a > b · a < b
Assert.InRange(<value>, <low>, <high>)                                      // low <= value <= high, inclusive
Assert.Matches(<pattern>, <text>)                                           // the text matches the regex pattern
Assert.NotEmpty(<collection>)           Assert.Count(<collection>, <n>)     // at least one · exactly n
Assert.That(<bool condition>, <value>)                                      // condition holds; value shown on failure
Assert.Throws(() => <expression>)       // the code faults — with ANY fault
Assert.Throws<T>(() => <expression>)    // …and it is a `T`; RETURNS the fault, so `.Message` / `.Type` read
Assert.Denied(() => <expression>)       // the acting principal is refused
Assert.Throws(() => { <statements> })   // a SEQUENCE that should fault (block body)
Assert.Denied(() => { <statements> })   // a sequence the acting principal is refused
Assert.Refuses(<assertion>)             // that ASSERTION fails — the one you point at a rule you want enforced
Assert.Refuses(<assertion>, <message>)  // …and the refusal says this (CONTAINS, not word for word)
```

## Description    {#description}

### The four you will reach for every time   {#everyday}
```osy title="asserting on what a function did" test app=testing-assert
entity Order {
  [Required] string Code;
  decimal Total;
  bool Cancelled;
}

void PlaceOrder(string code, decimal total) {
  var o = new Order { Code = code, Total = total };
}

[Test]
void An_order_starts_uncancelled() {
  PlaceOrder("A1", 42m);
  var o = Order.Single(x => x.Code == "A1");

  Assert.Equal(42m, o.Total);        // expected first, actual second — as in xUnit
  Assert.False(o.Cancelled);
  Assert.NotNull(o.Code);
}
```

`Assert.Equal(expected, actual)` — expected first. Get it backwards and the test still passes; it is the *failure
message* that lies to you, which you will discover at the worst moment.

### Comparing a `double` — `Assert.Equal(expected, actual, precision)`   {#precision}
The third argument is the number of **decimal places** both sides are rounded to before comparing — xUnit's own
overload. Reach for it whenever the value is a `double`:

```osy syntax
Assert.Equal(0.0, Math.Sin(Math.PI), 12);
Assert.Equal(2.0, Math.Log(Math.Exp(2)), 12);
```

`Math.Sin(Math.PI)` is **not** exactly 0, and `Math.Log(Math.Exp(2))` is not exactly 2 — in any language. π and e are
not representable as doubles, so every transcendental inherits the error of its input. The tolerance is not
sloppiness; it is the only correct way to assert one.

A `decimal` pair rounds **as decimal**, never through a double — money is exactly where a detour through binary
floating point would reintroduce the imprecision you are trying to tolerate. And the two-argument form stays
**exact**: the precision widens the comparison only where you ask for it.

### `Assert.Throws` — prove the rule bites   {#throws}
A rule you never tested is a rule you *hope* you wrote. Assert that breaking it actually fails:

```osy title="an invariant is really enforced" test app=testing-assert
entity Account {
  [Required] string Holder;
  decimal Balance;
  invariant Balance >= 0;
}

[Test]
void A_negative_balance_is_refused() {
  Assert.Throws(() => new Account { Holder = "Ada", Balance = -1m });
}
```

Without this test, deleting the `invariant` line breaks nothing that anyone notices — until a balance goes negative in
production.

### `Assert.Throws<T>` — and prove it is the RIGHT fault   {#throws-typed}
`Assert.Throws(…)` holds for **any** fault, which is often one claim too weak: a test that means "this is refused
because the row already exists" also passes when the code under test throws because a field is null. Name the type
and the assertion says which:

```osy title="the type is part of the claim" test app=testing-assert
entity Invitation {
  [Required, Unique, MaxLength(40)] string Code;
}

[Test]
void A_duplicate_code_is_refused() {
  var first = new Invitation { Code = "welcome" };
  var ex = Assert.Throws<ValidationException>(() => new Invitation { Code = "welcome" });
  Assert.Contains("Code", ex.Message);
}
```

Two things it gives you that the untyped form does not. It **fails on the wrong fault** — if the code throws a
`NotFoundException`, the test says so by name instead of passing. And it **returns the fault**, so you can go on to
assert on `ex.Message` (what a human will read) and `ex.Type` (its name as a string).

The type argument is the same closed set `throw` uses ([throw](https://osysharp.com/reference/function/throw/)), and only these nine names:

| Type | What raises it |
|---|---|
| `Exception` | the base — matches every one below, exactly like the untyped form |
| `NotFoundException` | you asked for something that does not exist (app-raised) |
| `ValidationException` | **the entity's own declared rules refused the write** — a broken `[Unique]` (a duplicate row, single-column or composite), `[Required]`, `[Pattern]`, `[MaxLength]`, `[Min]`, `[Max]`, or an invariant. The platform raises this for you at the commit, so it is the one most tests assert |
| `ConflictException` | the CURRENT STATE of the data refuses it — two writers colliding. **Not** a broken constraint, however much English calls a double booking a conflict |
| `OAuthConnectionFailedException` | an external authorization handshake failed |
| `NotAuthorized` | engine-raised — a workflow event's `[Authorize]`, or a slot's candidate gate, said no |
| `RequirementsNotMet` | engine-raised — a slot deposit's `Requires` criteria are not met yet |
| `WorkflowError` | engine-raised — an awaited child workflow reached a `terminal error` |
| `WorkflowCancelled` | engine-raised — an awaited child workflow reached a `terminal cancel` |

Anything else is a compile error that lists the set with these same meanings, so you cannot misspell your way into a
silent catch-all.

⚠ A **security** refusal is [[testing-assert#denied|`Assert.Denied`]], not a type argument here — there is no
`SecurityException` in the set, deliberately.

The block-body form works the same way: `Assert.Throws<ValidationException>(() => { … })`.

### `Assert.Denied` — prove security bites   {#denied}
The security equivalent, and the most valuable assertion in the set. It claims that the **acting principal is
refused** — not that the code faulted, but that it was *not allowed*:

```osy title="the model: a memo only its owner may read" test app=testing-assert
[Principal] entity User {
  [Required] string Name;
}

entity Memo {
  User Owner;
  [MaxLength(200)] string Note;
  security {
    allow read when IsAuthenticated;      // colleagues can SEE it …
    allow update where Owner == user;     // … only the owner may change it
  }
}

void Annotate(Guid memoId, string note) {
  var m = Memo.Single(x => x.Id == memoId);
  m.Note = note;
}

```

The test — and every line of the setup is placed so that it cannot be the thing that fails:

```osy title="a user cannot touch another user's row" run app=testing-assert
// A `principal` resolves UNSECURED, which is what makes it usable here: a `[Test]` body outside a `runas` is an
// anonymous caller, so a `User.Single(…)` written there reads nothing and there is nobody to become.
principal Alice => User.Single(u => u.Name == "Alice");
principal Bob   => User.Single(u => u.Name == "Bob");

[TestFixture]
void Seed() {
  var alice = new User { Name = "Alice" };
  var bob = new User { Name = "Bob" };
  var memo = new Memo { Owner = alice, Note = "original" };
}

[Test(Seed)]
[runas(Alice)]
void Bob_cannot_annotate_Alices_memo() {
  var memo = Memo.Single(m => m.Owner == Alice);

  runas(Bob) {
    // Bob can READ this row — that is deliberate, and it is what makes the assertion mean something. The only
    // thing he may not do is WRITE it, so the refusal `Assert.Denied` catches can only be the update rule.
    Assert.NotNull(Memo.FirstOrDefault(m => m.Id == memo.Id));
    Assert.Denied(() => Annotate(memo.Id, "hijacked"));
  }
}
```

This is the test that stops a refactor from quietly opening a door. Write one for every rule that matters — see
[runas](https://osysharp.com/reference/testing/runas/).

Notice where the setup lives: Alice, Bob and the memo are created by the fixture, and the row is looked up as
**Alice** before the `runas(Bob)`. Notice too that `Memo` is READABLE by any colleague — if a non-owner could not
even see the row, `Annotate` would fail looking it up and `Assert.Denied` would pass on a refusal that has nothing
to do with the update rule the test names. Only the one thing that must be refused is inside `Assert.Denied`. That is not style — it is what makes the
assertion mean anything.

**When an UPDATE is refused.** A write is judged when it reaches the store, not when the property is assigned — so
inside `Assert.Denied` use the block form and commit in it: `Assert.Denied(() => { memo.Note = "x"; UnitOfWork.Commit(); })`.
A bare assignment with no commit inside the lambda is not yet a write the rule can refuse, and the assertion would
report that nothing was denied.

> **A denial test is the one test whose green tells you nothing by itself.** Every other assertion proves it ran by
> producing the right answer; this one is satisfied by *any* refusal on the way to the thing you are testing. Build a
> prerequisite inside the lambda and the platform may refuse **that** instead — the test passes, the rule you named
> was never evaluated, and nothing distinguishes the two.
>
> So: everything in the setup must be independently known-**allowed** for the acting principal. Seed prerequisites in
> a `[TestFixture]`, or create them as a principal already permitted to. And pair a denial with a **positive twin**
> that does the same thing successfully — if both fail the same way, the setup is what you are testing.

```osy title="the denial that proves nothing" syntax
// ✗ Creating the User is itself a write Alice may be refused for. If it is, the denial fires there
//   and Membership's rule is never reached — green, and about nothing.
runas(alice) {
  Assert.Denied(() => new Membership { Member = new User { Name = "Mallory" } });
}
```

`osy lint` reports this as **`testing-denial-provable-by-its-setup`**: a denial whose lambda constructs more than one
entity. Constructing exactly the subject is the correct shape and is never flagged.

### A block body — a sequence that should fault   {#block-body}
When the code you expect to fail is more than one expression — set something up, *then* do the thing that must be
refused — give `Assert.Throws` / `Assert.Denied` a **block** lambda instead of a single expression:

```osy title="a block body: arrange, then the act that must fault" test app=testing-assert
entity Ledger {
  [Required] string Name;
  decimal Balance;
  invariant Balance >= 0;
}

[Test]
void An_overdraw_is_refused() {
  var l = new Ledger { Name = "ops", Balance = 100m };
  Assert.Throws(() => {
    var current = l.Balance;      // a local
    l.Balance = current - 250m;   // the write that trips `invariant Balance >= 0`
  });
}
```

The block runs its statements in order and the assertion holds if **any** of them faults. Keep it a simple sequence —
locals, assignments and calls (the work you expect to throw). Control flow (`if`, `foreach`) and `return` don't belong
in a deferred-assert block; if you need them, put them in a helper function and call it inside the lambda.

A block lambda is accepted **only** in these deferred-assert positions. Everywhere else — notably a query predicate
like `Order.Where(o => …)`, whose body lowers to SQL — a lambda takes an **expression** body (`o => o.Total > 0`), and a
block there is a compile error that says so.

### Comparisons, collections, and a general predicate   {#more}
Beyond equality there is a small kit for the everyday shapes of a claim:

- **`Assert.Greater(a, b)`** and **`Assert.Less(a, b)`** — the first value is strictly greater / less than the second.
- **`Assert.InRange(value, low, high)`** — the value lies within `[low, high]`, **bounds included**.
- **`Assert.Matches(pattern, text)`** — the `text` matches the regular expression `pattern` — pattern first
  ([Regex](https://osysharp.com/reference/stdlib/regex/)).
- **`Assert.NotEmpty(collection)`** — the collection has at least one element; its opposite is `Assert.Empty`, and
  `Assert.Single` claims exactly one.
- **`Assert.Count(collection, n)`** — the collection has exactly `n` elements. This is the honest way to assert "the
  query returned three rows", and under a security rule it counts only the rows the acting principal may see.
- **`Assert.That(condition, value)`** — the escape hatch: assert an arbitrary boolean `condition` (first), with a
  `value` carried along to appear in the failure message. Use it for a claim none of the named assertions captures.

```osy title="the comparison and collection assertions, run" run app=testing-assert
[Test]
void Comparison_and_collection_assertions() {
  Assert.Greater(5, 3);                        // 5 > 3
  Assert.Less(3, 5);                           // 3 < 5
  Assert.InRange(5, 1, 10);                    // within [1, 10]
  Assert.InRange(1, 1, 10);                    // the bounds are inclusive
  Assert.Matches("[a-z]+[0-9]+", "abc123");    // pattern first: "abc123" matches the regex

  var parts = Text.Split("a,b,c", ",");        // a List<string> of three
  Assert.NotEmpty(parts);
  Assert.Count(parts, 3);                      // exactly three elements

  var total = 42m;
  Assert.That(total > 0m && total < 100m, total);   // condition first; `total` is shown if it fails
}
```

Prefer a named assertion when one fits — `Assert.Count(rows, 3)` reads better and fails with a clearer message than
`Assert.That(rows.Count == 3, rows.Count)`. Keep `Assert.That` for the claim that has no better name.

### The assertions that read the SCREEN   {#ui}

These nineteen live on the same `Assert.` and are absent from everything above — a test that drives the UI reaches
for them, and a reader who came here for "what can I assert?" would otherwise conclude they do not exist. Their
detail, and the `Ui.*` verbs that drive the screen they read, are in [Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/).

| assertion | says |
|---|---|
| `Assert.Visible(text)` | the text is rendered on the current screen |
| `Assert.Hidden(text)` | it is not |
| `Assert.TextIs(text)` | some element reads EXACTLY this — not merely contains it |
| `Assert.OnPage(path)` | the app is currently showing this route |
| `Assert.Value(field, expected)` | the field this label names holds this value |
| `Assert.Enabled(control)` · `Assert.Disabled(control)` | the control this label names is (not) operable |
| `Assert.DisabledBecause(control, reason)` | it is not operable, and explains itself with this sentence |
| `Assert.Items(container, expected)` | the list this label names shows exactly this many items |
| `Assert.Dialog(title)` | a dialog is open, and this is its title |
| `Assert.Checked(field[, expected])` | the checkbox or toggle is in this state — **checked** unless you pass `false` |
| `Assert.Expanded(control[, expected])` | the disclosure control is open (or closed) — **open** unless you pass `false` |
| `Assert.Selected(option[, expected])` | the option says it is the chosen one (or is not) — **chosen** unless you pass `false` |
| `Assert.Focused(control)` | this is the control the keyboard is on |
| `Assert.Before(first, second)` | the first value's row is rendered ABOVE the second's — the assertion a SORT needs |
| `Assert.Cell(row, column, expected)` | that row's cell under this column header reads this |
| `Assert.Probe(control, field, expected)` | a foreign control reports this field of its `probe { }` block as this |
| `Assert.Flow(container, direction)` | that container lays its children out `"across"` or `"down"` |
| `Assert.Violation(field[, message])` | this field is refused, and the message it shows contains this |

**Any of them may be scoped to one region** with `within:` — `Assert.Value("Name", "Ada", within: "Edit book")` —
except the three a region cannot narrow: `OnPage` (a route is not inside a container), `Dialog` (a modal is
page-level), and `Violation`. `Ui.Within(container) { … }` scopes a whole block at once.

## See also       {#see-also}
- [Layout assertions — is it actually usable on screen?](https://osysharp.com/reference/testing/ui-layout/) — the geometric assertions (`Assert.Clickable`, `Assert.FitsOn`, `Assert.Above`, …), which need a renderer with a compositor and report NOT CHECKED without one
- [Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/) — the `Ui.*` verbs, and the detail of every screen-reading assertion above
- [Regex](https://osysharp.com/reference/stdlib/regex/) — the pattern language `Assert.Matches` uses
- [[Test] / [TestFixture]](https://osysharp.com/reference/testing/test/) — the `[Test]` functions these assertions live in
- [runas](https://osysharp.com/reference/testing/runas/) — acting as a principal, so `Assert.Denied` has someone to deny
- [invariant](https://osysharp.com/reference/entity/invariants/) — the rules `Assert.Throws` proves you wrote


---

<!-- https://osysharp.com/reference/testing/api-calls/ -->

# Calling your own REST API from a test

> `Api.*` sends a real HTTP request to a route your own app publishes with `app.Apis`, from inside a `[Test]`, and returns what came back — `StatusCode`, `Body`, `IsSuccess`, `Headers`. The request goes through the same handler a stranger's `curl` reaches, so it exercises routing, authentication, JSON binding and the refusal-to-status mapping. It is ANONYMOUS unless you name a credential, because that is what a third party is.

<!-- id: testing-api-calls · area: testing · stability: preview · html: https://osysharp.com/reference/testing/api-calls/ -->

## Summary        {#summary}
[`app.Apis`](https://osysharp.com/reference/api/rest/) publishes your app over HTTP for other systems to call. `Api.*` is how you **call it back** —
from your own `.test.osy`, over real HTTP, into the same handler a stranger reaches.

That matters because everything interesting about a published API happens on the way IN: the route has to resolve, the
credential has to be accepted, the JSON body has to bind to your function's parameters, and a refusal has to come back
as the right status. Calling the function directly proves none of it. `Api.*` proves all of it, and the assertion is
an ordinary `Assert.Equal` over a number:

```osy syntax
var r = Api.Post("/api/rest/v1/lab/specimens", "{\"accession\":\"ACC-1\"}");
Assert.Equal(201, r.StatusCode);
```

## Signature      {#signature}
```osy title="the five verbs, and where a credential goes" syntax
Api.Get(path)                  Api.Delete(path)
Api.Post(path, body)           Api.Put(path, body)           Api.Patch(path, body)

// every verb also takes a body, and every verb also takes either credential — both BY NAME
Api.Post(path, body, apiKey: "pk_…")
Api.Get(path, bearer: token)
```

`path` and `body` are positional; `apiKey:` and `bearer:` are named-only. `body` is optional on **every** verb — a
call with no body is simply the one-argument form.

Two more verbs answer the question those arguments raise — *where do I get one?*:

```osy title="where a credential comes from — you name the principal, not the secret" syntax
Api.KeyFor(P)      // that principal's API key (`pk_…`), for `apiKey:`
Api.TokenFor(P)    // a bearer token for that principal, for `bearer:`
```

Each takes a declared [`principal`](https://osysharp.com/reference/testing/runas/) — the same operand as `Ui.SignInAs(P)`, and for the same reason:
it names WHO, and who is a row your fixture seeded, never a string somebody typed.

Each call returns an `ApiResponse`:

| member | what it is |
|---|---|
| `StatusCode` | the status your app answered with — `200`, `201`, `400`, `401`, `403`, `404`, `409`, `500` |
| `Body` | the response body as text. `JsonSerializer.Deserialize<T>(r.Body)` reads it into a `class` you declare |
| `IsSuccess` | true when the status is 2xx |
| `Headers` | every response header, as `ApiHeader { Name, Value }` — `r.Headers.Single(h => h.Name == "Location").Value` |

A 4xx or 5xx is a **normal return**, never a throw. Asserting that a refusal carries the right status is the whole
point of the verb, and a throw would make exactly those cases unassertable.

## Description    {#description}

### Which path do I call?   {#path}
The **published** one. An `Endpoint`'s own `Path = "/scans"` is only the tail of it; the address the world calls is
built from the API's `Route`, its major `Version` (v1 when you omit it), and then that tail:

```text
/api/rest/v{major}/{route}/{the Endpoint's Path}          →  /api/rest/v1/carrier/scans
/api/rest/v{major}/{route}/entities/{Entity}              →  /api/rest/v1/vault/entities/Specimen
```

`osy model` prints each API's base path, so the reliable move is to copy it from there. Writing the short form is a
compile error that spells the full shape out — it is refused rather than 404'd, because a 404 reads as "my route is
broken" and there is nothing in it to correct you with.

### Who is the caller?   {#who-is-calling}
**Nobody, unless you say otherwise.** A request with no `apiKey:` and no `bearer:` arrives anonymous, exactly as a
stranger's does.

⚠ **`runas` does not reach it, deliberately.** [`runas(P)`](https://osysharp.com/reference/testing/runas/) rebinds the principal on the ENGINE; the
request is a separate call arriving at your front door with whatever it carries. If the enclosing `runas` leaked into
it, an authorization test would go green because the ENGINE was somebody — on a request that presented nothing. That
is a test passing while the door stands open, so the verb refuses to make it possible.

Which means the negative test is the easy one, and it is the one worth writing first:

```osy syntax
// no credential → the app's own gate answers, and it is the gate you are testing
Assert.Equal(401, Api.Get("/api/rest/v1/vault/entities/Specimen").StatusCode);
```

To be somebody, name a credential. The two map to the two things [[api-rest#who-is-user|`ApiAuth`]] accepts:

| you write | it is sent as | it satisfies | where the value comes from |
|---|---|---|---|
| `apiKey:` | `X-API-Key` | `new ApiAuth { ApiKey = true }` | `Api.KeyFor(P)` |
| `bearer:` | `Authorization: Bearer …` | `new ApiAuth { Bearer = true }` (and `OAuth`) | `Api.TokenFor(P)`, or your own `[AuthMethod]` |

An authenticated caller is **a user of your app** — the same `user`, the same role grants, the same row filters as
that person signed in. Nothing about an API call is a special machine identity.

### Where a credential comes from   {#credentials}
Both mints are **test-only**, and the compiler refuses them outside a `[Test]`/`[TestFixture]`. Handing out a working
credential for a principal without presenting one is impersonation anywhere else; it is legitimate here because a
fixture seeded that row and naming it is an authoring act — the same rule, and the same reason, as `Ui.SignInAs`.

`Api.KeyFor(P)` mints a real per-user API key, exactly as `osyrin app user apikey generate` does for a live app, and
stores its hash on the principal's own row — so the app's `[Principal]` must declare the two columns
[[api-rest#api-key-storage|`ApiKeyHash` and `ApiKeyHash2`]], as it must to accept keys at all. It is **stable for the
life of the run**: ask twice and you get the same key. That is deliberate — a principal has exactly two key slots (the
second is the rotation one), so a verb that minted afresh on every mention would run out on the third call.

`Api.TokenFor(P)` mints a bearer token — the same ticket `Ui.SignInAs(P)` signs the browser in with, so a test that
drives a page and a test that calls the API as the same person hold one credential, not two that could disagree.

⚑ **Your app's own login also works, and is often the better test.** `Security.IssueJwt` — what an `[AuthMethod]`
returns — issues a real ticket in a served run, so `bearer: Login(email, password)` calls your API as somebody who
authenticated the way a real caller does, through your own password check:

```osy syntax
Api.Get("/api/rest/v1/vault/entities/Specimen", bearer: Login("curator@lab.test", "hunter2"))
```

Reach for `Api.TokenFor(P)` when the app has no login to call, or when the test is about the API rather than about
signing in.

### What can run this?   {#where-it-runs}
A runner that is actually **serving** your app. `osy test` is one — it starts or finds a local platform for you, so
nothing is required of you beyond writing the call. An engine-only runner has no door to knock on, and there the test
is **skipped with a reason, before its body runs at all** — never a red. That distinction is the whole point: an error
part-way through a body reads as "this app is broken" and sends you to read an endpoint that is working, while a skip
reads as "this runner does not do that". It is decided up front, from the source, and it follows helpers: a test that
posts through a `Signed(...)` of your own is still a test that calls your API.

That applies to `Api.KeyFor` / `Api.TokenFor` as well, and for the same reason: a key is hashed against the id of the
app being served and a token is signed with the platform's key, so only a runner that IS the server can make either.
A runner that cannot says so in a sentence, rather than handing back a credential nothing will accept.

⚑ **Every fence on this page is `test`, not `run` — deliberately.** A `run` fence is EXECUTED by the docs gate, and
the docs harness has a database and no web server, so every `Api.*` call in one would fail on the missing door rather
than on anything the example got wrong. `test` compiles them, which is the strongest check this harness can honestly
make: it catches a wrong verb, a wrong argument name, a bad path and a type error. The behaviour they describe — every
status below, and the credentialed calls above — is executed for real by the platform's own acceptance suite for this
verb, which runs against a live host.

### Why isn't this `Http.Post`?   {#not-http}
Because they are opposite directions, and conflating them costs you a capability you do not want:

| | [`Http.*`](https://osysharp.com/reference/testing/outbound-calls/) | `Api.*` |
|---|---|---|
| direction | OUT, to somebody else's host | IN, to a route **your** app publishes |
| gate | `use Osysharp.Http;` — an egress capability you declare | none; publishing an API is enough |
| returns | `HttpResponse` (needs `using Osysharp.Http;`) | `ApiResponse`, always in scope |
| available in app code | yes | no — there is nothing for an app to gain by calling its own endpoint |

A loopback `Http.Post("http://localhost/…")` is not a workaround for this: the platform's egress guard refuses an
internal address, which is correct and is why this verb exists.

## Examples       {#examples}
The app: one entity with a `[Unique]` column, one function published as a route, and a second API gated on an API key.

```osy title="an app that publishes an open route and a gated one" test app=testing-api-calls
// The two columns an API key needs somewhere to live. Declaring `ApiAuth { ApiKey = true }` without them is a
// compile error — no key could be minted and none could be verified, so the route would be a 401 forever.
[Role] enum LabRole { Authenticator, Curator }
entity RoleGrant {
  [Required("Name the curator this grant belongs to.")] Curator Grantee;
  [Required("Choose the role this grant confers.")] LabRole Level;
  security { allow read when IsAuthenticated; }
}
policy IsAuthenticator => RoleGrant.Any(g => g.Grantee == user && g.Level == LabRole.Authenticator);

[Principal] entity Curator {
  [Unique, MaxLength(200)] string Email;
  [MaxLength(255)] string? ApiKeyHash;
  [MaxLength(255)] string? ApiKeyHash2;
  security {
    allow read when IsAuthenticated;
    // ⛔ BOTH SLOTS. The platform ships the [Principal] to the client for `Session.CurrentUser.*` minus its
    //    MASKED properties, so a key hash with no field-level `deny read` rides that payload to the browser.
    deny read ApiKeyHash  when !IsAuthenticator;
    deny read ApiKeyHash2 when !IsAuthenticator;
  }
}

entity Specimen {
  [Required, MaxLength(50), Unique("That accession number is already recorded.")] string Accession;
  [MaxLength(100)] string Species;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

class SpecimenReceipt {
  [MaxLength(50)] public string Accession;
  [MaxLength(100)] public string Species;
}

// The wrapper a LIST route needs — see "Reading the body back into a class" below. `Result` binds the `result`
// the platform wraps a non-entity, non-class return in.
class SpecimenPage { public List<SpecimenReceipt> Result; }

[AllowAnonymous]
SpecimenReceipt RecordSpecimen(string accession, string species) {
  if (accession == "RETIRED") {
    throw new ConflictException("That accession number was retired and cannot be reused.");
  }
  var s = new Specimen { Accession = accession, Species = species };
  return new SpecimenReceipt { Accession = s.Accession, Species = s.Species };
}

[AllowAnonymous]
List<SpecimenReceipt> ListSpecimens() {
  return Specimen.OrderBy(s => s.Accession)
                 .Select(s => new SpecimenReceipt { Accession = s.Accession, Species = s.Species })
                 .ToList();
}

app.Apis = [
  new RestApi("Lab") {
    Route = "lab",
    Endpoints = [
      new Endpoint(RecordSpecimen) { Method = HttpMethod.Post, Path = "/specimens", SuccessStatus = 201 },
      new Endpoint(ListSpecimens)  { Method = HttpMethod.Get,  Path = "/specimens" },
    ],
  },
  // Gated. It accepts either credential, so one route exercises both columns of the table above.
  new RestApi("Vault") {
    Route  = "vault",
    Auth   = new ApiAuth { ApiKey = true, Bearer = true },
    Expose = [ new Crud<Specimen>() { Operations = [CrudOp.Read] } ],
  },
];
```

```osy title="the happy path — and the row it really wrote" test app=testing-api-calls
[Test]
void a_post_to_the_published_route_runs_the_function_and_the_row_lands() {
  var r = Api.Post("/api/rest/v1/lab/specimens", "{\"accession\":\"ACC-1\",\"species\":\"Bufo bufo\"}");

  Assert.Equal(201, r.StatusCode);            // the endpoint's own SuccessStatus, not a generic 200
  Assert.Contains("Bufo bufo", r.Body);       // the function's return value came back down the wire
  Assert.Contains("json", r.Headers.Single(h => h.Name == "Content-Type").Value);

  // …and an ordinary query in the same test reads the row the request created.
  Assert.Equal("Bufo bufo", Specimen.Single(s => s.Accession == "ACC-1").Species);
}
```

```osy title="every refusal, as the status it is documented to be" test app=testing-api-calls
[Test]
void each_refusal_carries_its_documented_status() {
  Assert.Equal(201, Api.Post("/api/rest/v1/lab/specimens", "{\"accession\":\"ACC-2\"}").StatusCode);

  // a [Unique] collision is the language's ValidationException → 400, and nothing is written twice
  Assert.Equal(400, Api.Post("/api/rest/v1/lab/specimens", "{\"accession\":\"ACC-2\"}").StatusCode);
  Assert.Equal(1, Specimen.Where(s => s.Accession == "ACC-2").Count());

  // a refusal the function THREW → 409, carrying its own message
  var conflict = Api.Post("/api/rest/v1/lab/specimens", "{\"accession\":\"RETIRED\"}");
  Assert.Equal(409, conflict.StatusCode);
  Assert.Contains("retired", conflict.Body);

  // a route nothing publishes → 404
  Assert.Equal(404, Api.Get("/api/rest/v1/nosuchapi/entities/Specimen").StatusCode);
}
```

### Reading the body back into a class  {#reading-the-body}
`Assert.Contains("Bufo bufo", r.Body)` above is a substring match on raw JSON, and it is the weakest thing you can
say about a response — it passes when the value is in the wrong field. Read the body into a `class` instead:

```osy title="the response, read into a class you declare" test app=testing-api-calls
[Test]
void the_response_body_reads_back_into_a_declared_class() {
  var r = Api.Post("/api/rest/v1/lab/specimens", "{\"accession\":\"ACC-DTO\",\"species\":\"Hyla arborea\"}");

  var receipt = JsonSerializer.Deserialize<SpecimenReceipt>(r.Body);
  Assert.Equal("ACC-DTO", receipt.Accession);
  Assert.Equal("Hyla arborea", receipt.Species);
}
```

The response spells its keys in **camelCase** (`"accession"`) while your class declares `Accession` — that is fine,
because names bind ignoring case, and an `[ExternalName("…")]` on a member overrides the spelling entirely.

⚠ **A route whose function returns a LIST does not answer a bare array.** Anything that is not an entity, a `class`
or `void` arrives inside the `{"result": …}` envelope ([[api-rest#envelope]]), so the DTO is a **wrapper**:

```osy title="a list endpoint answers an envelope — read through the wrapper" test app=testing-api-calls
[Test]
void a_list_endpoint_answers_the_result_envelope() {
  Api.Post("/api/rest/v1/lab/specimens", "{\"accession\":\"ACC-L1\",\"species\":\"Rana\"}");
  Api.Post("/api/rest/v1/lab/specimens", "{\"accession\":\"ACC-L2\",\"species\":\"Bufo\"}");

  var page = JsonSerializer.Deserialize<SpecimenPage>(Api.Get("/api/rest/v1/lab/specimens").Body);
  Assert.Equal(2, page.Result.Count);
  Assert.Equal("ACC-L1", page.Result[0].Accession);
}
```

If a body matches **none** of a class's members, `Deserialize` refuses and names both vocabularies — what your class
declares and what the body actually carries — rather than handing you an object with every field null. A member that
matched nothing while the body carried a key you did not declare is reported through `osy logs` and on the test's own
notes, without refusing: a small DTO over a large response is the ordinary case.

```osy title="the same door, with a credential" test app=testing-api-calls
[TestFixture]
void Seed() {
  new Curator { Email = "curator@lab.test" };
}

principal Chief => Curator.Single(c => c.Email == "curator@lab.test");

[Test(Seed)]
void a_named_principals_credential_opens_the_gated_route() {
  // Nobody gets in…
  Assert.Equal(401, Api.Get("/api/rest/v1/vault/entities/Specimen").StatusCode);

  // …and so does a credential that is not the real one, which is what makes the two below mean something.
  Assert.Equal(401, Api.Get("/api/rest/v1/vault/entities/Specimen", apiKey: "pk_not-a-real-key").StatusCode);

  // Either credential, naming the principal it belongs to.
  Assert.Equal(200, Api.Get("/api/rest/v1/vault/entities/Specimen", apiKey: Api.KeyFor(Chief)).StatusCode);
  Assert.Equal(200, Api.Get("/api/rest/v1/vault/entities/Specimen", bearer: Api.TokenFor(Chief)).StatusCode);
}
```

```osy title="a third party writing through the API, colliding with a row the app made" test app=testing-api-calls
[Test(Seed)]
void a_third_party_writing_through_the_api_collides_with_a_row_the_app_made() {
  // In-app: an ordinary engine-side write.
  runas (Chief) { new Specimen { Accession = "ACC-SHARED", Species = "Bufo bufo" }; }

  // Over the wire: the same accession, arriving at the front door with a credential. The app's own `[Unique]`
  // sentence comes back as the status it is documented to be, and nothing is written twice.
  var r = Api.Post("/api/rest/v1/lab/specimens",
                   "{\"accession\":\"ACC-SHARED\",\"species\":\"Rana\"}",
                   apiKey: Api.KeyFor(Chief));
  Assert.Equal(400, r.StatusCode);
  Assert.Equal(1, Specimen.Where(s => s.Accession == "ACC-SHARED").Count());
}
```

## See also       {#see-also}
- [publishing a REST API (app.Apis)](https://osysharp.com/reference/api/rest/) — `app.Apis`: publishing the routes this calls, and the full refusal-to-status table
- [Outbound calls in a test](https://osysharp.com/reference/testing/outbound-calls/) — `Http.*`, the other direction: calling somebody else's host from a test
- [runas](https://osysharp.com/reference/testing/runas/) — being somebody in the test BODY, and why that never travels to the request
- [Assert](https://osysharp.com/reference/testing/assert/) — `Assert.Equal` / `Assert.Contains`, which is how a response is read
- [Testing (real app, real data, real rules)](https://osysharp.com/reference/testing/index/) — what a test runs against, and who it acts as


---

<!-- https://osysharp.com/reference/testing/debugging-tests-locally/ -->

# Debugging tests locally

> Debugs one of your app's tests against a Platform on your own machine — breakpoints, stepping, and variable inspection in your editor — with no account and no network. `osyrin dev` starts the platform; your editor launches the debugger through `osy debug-test`.

<!-- id: testing-debugging-tests-locally · area: testing · stability: stable · html: https://osysharp.com/reference/testing/debugging-tests-locally/ -->

## Summary        {#summary}

Debugs a single test against a Platform running on your own machine. You set a breakpoint in a test, launch the
debugger from your editor, and the run pauses where you asked — with the call stack, the current line, and your
variables all inspectable. It is the debugging counterpart of [Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/): same local platform,
same throwaway-copy isolation, same enforced security — you are simply watching one test run under a debugger instead
of reading a pass/fail report.

## Signature      {#signature}

```osy syntax
osy debug-test --test <id> [path]
```

## Description    {#description}

### Debug from your editor   {#editor}

With a local platform running (`osyrin dev`), open a test file in an editor that has the Osy# extension, set a
breakpoint on a line inside a `[Test]`, and start debugging. The editor launches the debugger for you and drives the
session; `osy debug-test` is the command it runs behind the scenes to reach the local platform. As with
[Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/), there is nothing to log into and nothing to deploy — it finds the running local
platform for your project, ensures your app exists there, compiles the source on your disk, and debugs against that.

### One test at a time   {#one-test}

A debug session runs exactly one test, named by its id (`file::fixture::name`) — the same id
[Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) uses with `--test`. That is why the flag is `--test <id>` and not repeatable: a
debugger pauses inside one test, not across a suite. Pick the test in your editor's test view, or pass its id.

### The test runs exactly as it would anywhere   {#parity}

Debugging is not a weaker mode. Your app's own security is enforced just as in production: a `[Test]` runs as an
anonymous, secured caller, so a test that reads or creates data needs your model to grant it. The test runs in its own
throwaway clone that is discarded when the session ends — nothing it writes survives, and your real data is never
touched. See [Running tests](https://osysharp.com/reference/testing/running-tests/) for the full model.

### Debugging against a remote platform   {#remote}

The same gesture works against a deployed app with `osyrin app debug-test --test <id>`. It is the remote twin of this
command, exactly as [Running tests](https://osysharp.com/reference/testing/running-tests/) is the remote twin of [Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) — the only
difference is which platform it reaches.

## Examples       {#examples}

Start a local platform, then debug one test from your editor (which invokes the command for you):

```console
# terminal 1
osyrin dev
```

```osy title="the model — behaviour on a class, so it can be stepped" test app=testing-debugging-tests-locally
// model/order.osy — behaviour lives on a class, so it can be stepped through.
class Order {
  public decimal Total;
  public void AddLine(decimal amount) { Total = Total + amount; }
}
```

```osy title="the test, and the line to put the breakpoint on" test app=testing-debugging-tests-locally
// tests/orders.test.osy
[Test]
void Totals_Add_Up() {
  var order = new Order { };          // ← set a breakpoint here, then start debugging
  order.AddLine(20);
  order.AddLine(5);
  Assert.Equal(25, order.Total);
}
```

The equivalent invocation the editor makes:

```console
osy debug-test --test "tests/orders.test.osy::Seeded::Totals_Add_Up"
```

## See also       {#see-also}

[Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) — run your tests locally without a debugger, and the full description of the local
platform, isolation, and enforced security.

[Running tests](https://osysharp.com/reference/testing/running-tests/) — the same run against a remote platform.


---

<!-- https://osysharp.com/reference/testing/ui-layout/ -->

# Layout assertions — is it actually usable on screen?

> Every other assertion is about TEXT or STATE, and all of them pass on a screen that is visually broken: a control can be on the page, enabled, correctly labelled, and still be under an overlay, off its own card, or too narrow for its own label. These eleven ask about GEOMETRY instead. They need a renderer with a compositor, so they are checked by `osy test --pixels` (a real browser) and report themselves NOT CHECKED under plain `osy test` — never green. `Ui.Shot("label")` photographs the page beside them.

<!-- id: testing-ui-layout · area: testing · stability: preview · html: https://osysharp.com/reference/testing/ui-layout/ -->

## Summary        {#summary}

`Assert.Visible("Confirm")` passes on a button that a person cannot press, because something is drawn on top of it.
`Assert.Enabled("Save")` passes on a button laid out past the edge of its own card. `Assert.Visible("Quarterly
revenue report")` passes on a chip rendering `Quarterly rev…`, because the DOM holds the whole string whether the box
shows it or not. Every assertion in [Assert](https://osysharp.com/reference/testing/assert/) is about text or state, and none of them can see any of that.

The eleven below ask about **geometry**: what covers what, what is inside what, what is wider than what, and whether
anything is cut off. They need a renderer that has a font engine and a compositor — so they are checked by
`osy test --pixels`, which drives a real browser, and under plain `osy test` they report themselves **NOT CHECKED**
rather than passing.

## Signature      {#signature}

```osy syntax
// is it USABLE?
Assert.Clickable(control);          // a click at its own centre reaches IT — nothing covers it, its box is not empty
Assert.Inside(control, container);  // its box lies within that container's
Assert.NoOverflow();                // nothing pushes the page wider than the window

// where things ARE
Assert.Above(first, second);        // …and Assert.Below
Assert.LeftOf(first, second);       // …and Assert.RightOf

// how big, relative to each other
Assert.Wider(first, second);        // …and Assert.Narrower
Assert.SameWidth(first, second);

// is any of it cut off?
Assert.FitsOn(control);

// and a photograph, which asserts nothing
Ui.Shot("the empty board");
```

```console
osy test                     # behaviour. Layout claims report NOT CHECKED — this renderer has no compositor.
osy test --pixels            # the same tests in a real browser. The claims are judged; `Ui.Shot` writes PNGs.
osy test --pixels --headed   # …and SHOW the browser, slowed down, so you can watch it drive the page.
```

## Description    {#description}

### Why these are separate from every other assertion   {#why}

A UI test asserts three different kinds of thing, and only two of them were expressible before:

| the question | the assertion | can a text renderer answer it? |
|---|---|---|
| is this text on the page? | `Assert.Visible` | yes |
| is this control enabled, checked, selected? | `Assert.Enabled`, `Assert.Checked` | yes |
| **can a person actually use it?** | `Assert.Clickable`, `Assert.FitsOn` | **no — it needs a compositor** |

The third row is not a nicety. A control that renders, satisfies every text assertion, and is unclickable is a
shipped bug that a full green suite reports as fine.

### NOT CHECKED is not passed   {#unchecked}

⛔ **A renderer that cannot judge a claim never reports it as holding.** Plain `osy test` renders in happy-dom, which
has no font engine and no compositor: every box it measures is 0×0 and nothing is ever on top of anything. So a
layout assertion there answers **NOT CHECKED**, with the flag that checks it:

```console
│ Assert.Clickable(…) — NOT CHECKED: this run renders in happy-dom, which has no compositor — every
  box it reports is 0×0, so nothing here can judge what covers what. Run the same tests with
  `osy test --pixels` to check them in a real browser.
```

The test carries on — its behavioural assertions are real and worth having — and the run keeps the count, so a green
tally can never read as *"the layout was checked"*.

⚑ **This is why one test file runs in both tiers.** The alternative — refusing — would make a `.test.osy` containing
a layout assertion runnable under `--pixels` and not under `osy test`, which is a worse product than having no tier
at all. You write one file; you choose the renderer at the command line.

### `Assert.Clickable` — the one to reach for first   {#clickable}

It resolves the control exactly as `Ui.Click` would, takes the point a click would land on, and asks the page what
is actually there. Three ways it refuses, each with the numbers:

- something is **covering** it — the refusal names what a press would hit instead;
- its box is **empty** (0×0) — in the DOM, taking up no space, pressable by nobody;
- it is laid out **off the window** entirely.

```console
Assert.Clickable(…) failed: a click at 'Confirm's own centre (68, 219) lands on <span> "half price
today" instead — something is covering it. That is what a person pressing it would hit, so the control
is on screen and unusable.
```

### `Assert.FitsOn` — the claim no text assertion can make   {#fitson}

`textContent` reads the whole string whether the box shows it or not, so a truncated label is **invisible** to every
other assertion in the language. This is the only one that can see it:

```console
Assert.FitsOn(…) failed: 'Quarterly revenue report for the northern region' has content 257px WIDER
than its box (box 90×24, content 347×24), so part of it is CUT OFF on screen. It reads "Quarterly
revenue report for the northern region" to the DOM, which is why every text assertion passes on it.
```

### No pixel counts, deliberately   {#no-pixels}

⛔ **There is no `Assert.Equal(200, <the card's width>)`, and that is the design rather than an omission.** An
absolute pixel breaks when a font ships, a browser rounds differently, a theme token moves by 2px, or the display
changes scale — and a noisy suite gets deleted, taking the good assertions with it.

Every assertion here is an **invariant**: it compares two things on the same screen, so it survives a restyle, a font
update and a browser version. `Assert.Wider(a, b)` still holds after you change every size in the theme;
`Assert.Equal(200, …)` does not.

### `Assert.Above` is not `Assert.Before`   {#above-vs-before}

[Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/)'s `Assert.Before` is about **document order** among the rows of a list, and it holds in either
renderer. These are about **where things are**. The two disagree exactly when the app reorders visually — a
`column-reverse`, a CSS `order:`, grid placement, absolute positioning — which is the case where the page reads one
way and the markup says another, and only these can see it.

### What a name refers to   {#what-a-name-names}

A locator names the thing that **reads** the words, not the box around it — the same rule every locator in
[Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/) follows. So `Assert.Wider("Sidebar", "Rail")` compares the two pieces of TEXT, and a 300px panel with
a short label measures as its label. That is correct and surprising, so the refusal prints the tag it measured and
names the fix: give the container a `label:` and name that, exactly as `within:` names one.

```osy syntax
Box(label: "Sidebar panel", w: 300) { Text("Sidebar"); }
Assert.Wider("Sidebar panel", "Rail panel");   // the PANELS — not the words inside them
```

### `Ui.Shot` — evidence, not an assertion   {#shot}

`Ui.Shot("label")` photographs the page. It asserts nothing and can never fail a test: under `--pixels` it writes a
PNG beside your project, and under plain `osy test` it records *"no image: this run renders in happy-dom, which has
no compositor"* and carries on. It never writes something that is not a photograph under a photograph's name.

Reach for it when you want to LOOK at what a test is driving — it is the fastest way to understand a layout failure,
and often faster than reading the assertion that caught it.

### Running the pixel tier   {#pixels}

`osy test --pixels` runs the **same tests** in a real browser instead of happy-dom. It is opt-in because it costs a
browser: the tier needs Playwright and a Chromium (or your system Chrome, which it prefers). The check runs **before
anything is compiled or booted**, so a machine that cannot run it is told in a second, with the command that fixes
it — never a stack trace a minute into a run.

Everything else is identical: the same locators, the same refusals, the same test file.

Add **`--headed`** to watch it: the browser is shown and each interaction is slowed down enough to follow, which is
usually the fastest way to understand why a locator refused. It is only meaningful beside `--pixels`, and says so
rather than being quietly ignored.

## Examples       {#examples}

A page carrying the two shapes these exist for — a button something is drawn over, and a label too long for its box
— and the tests that catch both. Every assertion here passes in a real browser except the two that are meant to
fail, and every one of them reports NOT CHECKED under plain `osy test`.

```osy title="the claims a text assertion passes straight through" test app=layout-assertions
using Osysharp.Ui;

[Composable]
component Clipped(string text = "") {
  variants {
    base { W = 90; Overflow = Overflow.Hidden; WhiteSpace = WhiteSpace.Nowrap; TextOverflow = TextOverflow.Ellipsis; }
  }
  render { Text(text); }
}

[Page("/checkout")]
[AllowAnonymous]
[Render(CSR)]
component CheckoutPage() {
  action Nothing() { }
  render {
    Stack(gap: 4, p: 6) {
      Text("Masthead");
      Card("Actions") { Button("Cancel", onPress: Nothing); }
      // A banner drawn over the button: on the page, enabled, correctly labelled, and unpressable.
      Box(position: Position.Relative) {
        Button("Confirm", onPress: Nothing);
        Box(position: Position.Absolute, top: 0, left: 0, w: 400, h: 60, z: 50) { Text("half price today"); }
      }
      Clipped("A label far too long to fit inside the box it was given");
      Text("Footer");
    }
  }
}

[Test]
void the_cancel_button_is_usable() {
  Ui.Visit("/checkout");
  Assert.Clickable("Cancel");            // nothing covers it, and its box is not empty
  Assert.Inside("Cancel", "Actions");    // it has not been laid out past its own card
}

[Test]
void the_page_reads_the_way_it_is_laid_out() {
  Ui.Visit("/checkout");
  Assert.Above("Masthead", "Footer");
  Assert.Below("Footer", "Masthead");
  Assert.NoOverflow();
  Ui.Shot("the checkout page");          // asserts nothing — a PNG under `--pixels`, a note without it
}
```

## See also       {#see-also}
- [Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/) — the verbs and the text/state assertions these sit beside
- [Assert](https://osysharp.com/reference/testing/assert/) — the general assertion vocabulary
- [Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) — running these, and what `--pixels` needs
- [layout primitives](https://osysharp.com/reference/ui/layout/) — `gap`, `align` and `justify`, which decide the geometry these measure
- [style props](https://osysharp.com/reference/ui/styling/) — the style props, including the three a truncated line needs


---

<!-- https://osysharp.com/reference/testing/outbound-calls/ -->

# Outbound calls in a test

> A `[Test]` reaches the network for real — `Http.*` and a typed `client { }` operation both run inside a test, against the live endpoint. So a test can prove the one thing a green suite otherwise cannot: that the call your app depends on actually works. `Http.<Verb>.Stub(url => …)` opts one verb out, for the assertions the network cannot give you on demand.

<!-- id: testing-outbound-calls · area: testing · stability: preview · html: https://osysharp.com/reference/testing/outbound-calls/ -->

## Summary        {#summary}

A `[Test]` runs with the same capabilities the app has. `Http.Get(...)` inside a test **makes the request**, and a
typed `client { }` operation does too. Nothing is stubbed by default, so a test that calls your fetch function and
reads back what it stored is telling you about the real endpoint.

That matters more than it sounds, because it is the one thing the rest of the suite cannot tell you. Every other
check — compile, lint, render, assert — passes just as happily when the URL is dead, the API key is missing or the
response shape changed. **A green suite is not evidence that the endpoint works unless a test called it.**

When you want a call answered from inside the test instead — offline, deterministic, or shaped like a failure the
endpoint will not produce on demand — register a stub for that verb: `Http.Get.Stub(url => …)`.

## Signature      {#signature}

```osy syntax
Http.<Verb>.Stub(url => <HttpResponse>);              // Get · Post · Put · Patch · Delete
Http.Post.Stub((url, body) => <HttpResponse>);        // the second parameter is the REQUEST body
```

## Description    {#description}

An app that reads a value from outside has two halves, and they fail differently:

| half | what breaks | what catches it |
|---|---|---|
| the CALL | a dead URL, a moved path, a missing key, a rate limit | only a test that makes the call |
| the PARSE | a changed response shape, a field that is now nested | a test with a canned body, or the real one |

The second half can be tested without the network: call the function that CONSUMES a response, handing it a body
you wrote — or stub the fetch and let your own fetch function run over an answer you chose. The first half cannot
be faked, and it is the half that fails silently in production, because a fetch that never arrives usually leaves
the app showing whatever it had before.

⚠ **A non-2xx is a normal return, not an exception.** `response.IsSuccess` goes false and the body holds whatever
the server sent; nothing throws, so a `try`/`catch` around the call will not see it. Assert on `IsSuccess`, and
remember that some APIs answer **HTTP 200 carrying an error document** — a status check alone will not catch those,
but reading back what your app STORED will.

Every outbound call is logged whatever happens — a 2xx at `Information`, anything else at `Warning`, with the
method, the URL (query redacted), the status and the duration. `osy logs` is where a failing call explains itself.

### What a failing test says about the network   {#the-journal}

Every outbound call the test caused is also collected for the duration of that ONE test and attached to its report
when it **fails** — a green verdict stays one line, because nobody reads the network trace of a test that passed.
It is on the red one that "the fetch 404'd" and "the fetch has not arrived yet" are otherwise the same sentence.

- **Both sides of the wire are in it.** A call your test body makes and a call the SERVER makes answering a request
  your test caused — a `Ui.Click`, an `Api.*` — land in the same journal. Most of what a UI test does is press
  buttons, so this is usually the half that has the answer.
- **A call that never got an answer is in it too**, marked `FAILED:` (DNS, connect, TLS, a timeout, a body over the
  cap) or `BLOCKED:` (a target the egress guard refuses — a loopback or internal address is the common one). Those
  are the outcomes most easily mistaken for "it has not finished yet".
- **Addresses are redacted**: scheme, host, port and path only. A query string is the routine carrier of an API key
  and a verdict gets quoted, pasted and committed.

### Stubbing one verb   {#stubbing}

`Http.<Verb>.Stub(λ)` registers the lambda as the deterministic implementation of that verb for the rest of the
test. It is written as a statement, like any other stub:

- **The lambda takes the URL**, and optionally the **request body** (null for `Get` and `Delete`) — one signature
  for every verb, so `Http.Post.Stub((url, body) => …)` reads the same way as `Http.Get.Stub(url => …)`.
- **It returns an `HttpResponse`** — `new HttpResponse { StatusCode = 404, Body = "" }`.
- **`IsSuccess` is derived from the status you set**, not taken from the object. A stub cannot claim a response the
  real client could never produce, so a test cannot assert that a 500 succeeded and pass.
- **A registration in a `[TestFixture]` reaches every test that declares it**, and a `.Stub` later in a test body
  overrides it — last registration wins.
- **The stubbed call still lands in the run's outbound journal**, marked `(stubbed)`. A suite whose calls are all
  answered inside it must still be able to say what it would have called, or the stub becomes the new place a
  wrong URL hides.
- **It is per verb.** `Http.Stub(…)` is refused: `Http` has five of them and there is nothing to imply.

⚑ **Stubbing is opt-in on purpose, and the default is not a detail.** A suite that stubs by default is a suite that
cannot fail when the endpoint does — which is the failure this page exists to name. Stub the calls whose subject is
the PARSE, and leave at least one test that really calls out.

## Examples       {#examples}

Prove the whole path — the call, the parse and the storage — by running your own fetch and reading back the row:

```osy title="prove the whole path — call, parse and storage" test app=outbound-calls-whole-path
using Osysharp.Http;

entity ExchangeRate {
  [Required] decimal GbpToEur;
  security { allow read, create when IsAnonymous; }
}

void RefreshExchangeRate() {
  var r = Http.Get("https://open.er-api.com/v6/latest/GBP");
  new ExchangeRate { GbpToEur = 1.17m };      // …parsed from `r.Body` in a real app
  UnitOfWork.Commit();
}

[Test]
void the_rate_fetch_stores_a_real_rate() {
  RefreshExchangeRate();                       // the app's own function: it calls out, parses, and writes
  var rate = ExchangeRate.FirstOrDefault();
  Assert.NotNull(rate);
  Assert.True(rate.GbpToEur > 0m, "a stored rate must be a real number");
}
```

Ask the endpoint what it actually answers, when you are not sure it is alive or keyless:

```osy title="ask the endpoint what it actually answers" test app=outbound-calls-probe-the-endpoint
using Osysharp.Http;

[Test]
void the_endpoint_is_reachable_and_keyless() {
  var r = Http.Get("https://open.er-api.com/v6/latest/GBP");
  Assert.True(r.IsSuccess, "expected a 2xx");
  Assert.Contains("\"rates\"", r.Body);        // a 200 carrying an error document would fail HERE, not above
}
```

Pin the response so the PARSE is asserted offline — and so the failures the endpoint will not perform on demand
can be tested at all:

```osy title="stub the fetch to assert the parse, and the failures" run app=outbound-calls-stub-the-response
using Osysharp.Http;

entity ExchangeRate {
  [Required] decimal GbpToEur;
  security { allow read, create when IsAnonymous; }
}

void RefreshExchangeRate() {
  var r = Http.Get("https://open.er-api.com/v6/latest/GBP");
  if (!r.IsSuccess) { Log.Warning("rate fetch failed with {Status}", r.StatusCode); return; }
  new ExchangeRate { GbpToEur = 1.17m };      // …parsed from `r.Body` in a real app
  UnitOfWork.Commit();
}

[Test]
void a_good_response_is_parsed_and_stored() {
  Http.Get.Stub(url => new HttpResponse { StatusCode = 200, Body = "{\"rates\":{\"EUR\":1.17}}" });
  RefreshExchangeRate();
  Assert.NotNull(ExchangeRate.FirstOrDefault());
}

[Test]
void a_rate_limit_stores_nothing() {
  Http.Get.Stub(url => new HttpResponse { StatusCode = 429, Body = "slow down" });
  RefreshExchangeRate();
  Assert.Null(ExchangeRate.FirstOrDefault());   // the guard held: no half-written row
}
```

⚠ **A test that calls out depends on somebody else's uptime.** That is the right trade for the one or two tests
that exist to prove the integration, and the wrong one for a suite of thirty. Keep the network in the tests whose
subject IS the network, stub the rest, and test everything downstream of the response against a body you supply.

## See also       {#see-also}

- [[Test] / [TestFixture]](https://osysharp.com/reference/testing/test/) — declaring a `[Test]` and what a fixture seeds
- [Http.*](https://osysharp.com/reference/http/facade/) — the `Http.*` surface itself
- [Testing (real app, real data, real rules)](https://osysharp.com/reference/testing/index/) — the rest of the testing surface


---

<!-- https://osysharp.com/reference/testing/running-tests/ -->

# Running tests

> Runs your app's tests against a throwaway copy of its database, reporting each test as it finishes. Your local source is what runs, so you never have to deploy to test a change.

<!-- id: testing-running-tests · area: testing · stability: stable · html: https://osysharp.com/reference/testing/running-tests/ -->

## Summary        {#summary}

Runs your app's tests against a throwaway copy of its database, reporting each test as it finishes. Your local source
is what runs, so you never have to deploy to test a change.

## Signature      {#signature}

```osy syntax
osyrin app test [path] [--filter <text>] [--test <id>] [--json]
```

## Description    {#description}

A test run never touches your application's real data. Before anything executes, the platform makes a private copy of
your app's database, compiles your local source and your tests into that copy, and throws the whole thing away when the
run ends. Nothing a test writes can outlive it.

Inside that copy the structure mirrors how you wrote your tests. Each `[TestFixture]` seeds its own branch
once. Every `[Test]` that names a fixture then gets its *own* private clone of that seeded branch, runs there,
and drops it. Two tests under the same fixture therefore never see each other's writes, no matter what order they run
in — and because nothing is shared, they run at the same time.

Results stream back one at a time, as each test finishes, rather than arriving in a batch at the end. A long suite
turns green in front of you.

### What runs is what you wrote   {#source}

The source on your disk travels with your tests. The run compiles them together, so a test always exercises the code
you are looking at — not whatever version happens to be deployed. There is no build-then-test cycle, and no way to be
fooled by a stale deployment.

### Tests only run against a Development app   {#development-only}

An application in **Production** mode refuses to run tests, and says so. This is deliberate and cannot be turned off
per-run.

The reason is that a run copies the application's data in order to test against it, and then executes your test code
with access to that copy. For an application holding real user data, that is not something to do casually. Switch the
application to Development mode, or run your tests against a local platform.

### Choosing what to run   {#filtering}

By default every test runs. Narrow it two ways:

- `--filter <text>` runs the tests whose names contain `text`.
- `--test <id>` runs exactly one test, named by its id (`file::fixture::name`). Repeat the flag for several.

A fixture is never filtered away. If a test survives your filter, the fixture it depends on still seeds — otherwise the
test could not run at all.

A test marked `[Skip("reason")]` is always reported, and never runs. That is the point of marking it: the skipped test
stays visible as a reminder that something is unfinished, instead of quietly disappearing.

### In the editor   {#editor}

The VS Code extension shows the same tests in its Test Explorer, grouped by file and fixture, updating as you type —
even while a file is mid-edit and does not yet parse. Run one test, one fixture, one file, or everything. A failing
assertion puts a marker on the assertion itself.

### Scripting a run   {#json}

`--json` writes one JSON object per line, in the order events happen, so a script can react to each test as it lands.
The command exits non-zero when any test fails or errors. Diagnostics go to standard error, leaving standard output as
a clean stream of events.

```json
{"event":"enqueued","data":{"testId":"tests/orders.test.osy::Seeded::Totals_Add_Up","name":"Totals_Add_Up"}}
{"event":"passed","data":{"testId":"tests/orders.test.osy::Seeded::Totals_Add_Up","durationMs":31}}
{"event":"done","data":{"passed":1,"failed":0,"errored":0,"skipped":0}}
```

Each `testId` is stable across edits: it is built from the file, the fixture, and the test's name, never from a line
number. Rearrange a file and the ids stay put.

A test that fails or errors says where: `failureSpan` is the file, line and column of the statement that went wrong,
and `message` is the whole sentence. `osy test` is a **builder's** surface, so a security denial says everything: the
plain sentence the app's own users would see, and then the entity, the verb, the rules it was judged by and the
caller. (A real end user gets only the first half — see [what a refused user is told](https://osysharp.com/reference/security/denial-messages/).) A denial judged at commit is
reported at the statement that **wrote** the refused row, which may be a function in another file — read `file`
before `line`. The console run prints the same location as `file:line:col`.

```json
{"event":"errored","data":{"testId":"tests/invitations.test.osy::TwoPeople::a_non_member_cannot_invite","durationMs":412,"message":"You do not have permission to create this invitation. — Create of 'Invitation' denied — its create rules are: `allow create where Membership.Any(m => (m.Org == Org) && (m.Member == user))`; the row being written did not satisfy any of them (caller: mia@invite.test).","trace":null,"failureSpan":{"file":"model/invitations.osy","line":22,"column":3,"length":48}}}
```

## Examples       {#examples}

Run everything in the current project:

```console
osyrin app test
```

Run one test by name, then exactly one by id:

```console
osyrin app test --filter Totals
osyrin app test --test "tests/orders.test.osy::Seeded::Totals_Add_Up"
```

A fixture and the tests that fork from it:

```osy test app=testing-running-tests
entity Customer {
  [Required] string Name;
}

entity Order {
  Customer Customer;
  decimal Total;
}

[TestFixture]
void Seeded() {
  var customer = new Customer { Name = "Ada" };
  new Order { Customer = customer, Total = 100 };
}

[Test(Seeded)]
void Totals_Add_Up() {
  Assert.Equal(100, Order.First().Total);
}

[Test(Seeded)]
void A_New_Order_Is_Not_Seen_By_Siblings() {
  new Order { Total = 5 };
  Assert.Equal(2, Order.Count());   // the seeded one plus this one — and no one else's
}
```

## See also       {#see-also}
- [[Test] / [TestFixture]](https://osysharp.com/reference/testing/test/) — `[Test]`, `[TestFixture]` and `[Skip]`: what marks a function a test, seeds a fixture, or parks a test
- [Assert](https://osysharp.com/reference/testing/assert/) — the assertions a `[Test]` body calls
- [[runas(Name)] test attribute and principal selectors](https://osysharp.com/reference/testing/runas-attribute/) — running a test as a named principal


---

<!-- https://osysharp.com/reference/testing/running-tests-locally/ -->

# Running tests locally

> Runs your app's tests against a Platform on your own machine — no account, no network, no setup beyond a running local platform. `osyrin dev` starts that platform; `osy test` runs your tests against it.

<!-- id: testing-running-tests-locally · area: testing · stability: stable · html: https://osysharp.com/reference/testing/running-tests-locally/ -->

## Summary        {#summary}

Runs your app's tests against a Platform running on your own machine — no account, no network, no cloud. It is the
local counterpart of [Running tests](https://osysharp.com/reference/testing/running-tests/): the same tests, the same throwaway-copy isolation, the same streamed
report. The only difference is where it runs.

## Signature      {#signature}

```osy syntax
osy test [path] [--filter <text>] [--test <id>] [--pixels] [--headed] [--json]
```

## Description    {#description}

### Start a local platform, then test against it   {#starting}

A local platform is a full Platform that runs on your machine, with a database it manages itself. Start one from your
project directory:

```console
osyrin dev
```

The first start downloads a small database bundle once; after that it is up in seconds. It binds to your machine only —
nothing outside can reach it — and it needs no account and no cloud project. Leave it running in a terminal.

In another terminal, run your tests against it:

```console
osy test
```

That is the whole loop: edit your source, `osy test`, watch it turn green. There is nothing to log into and nothing to
deploy — `osy test` finds the running local platform for your project on its own, ensures your app exists there,
compiles the source on your disk into it, and runs your tests.

### `--pixels` — the same tests, in a real browser   {#pixels}

`osy test` renders your pages in a headless DOM with no font engine and no compositor. That is what makes it fast
enough to run on every change, and it means the run is **blind to layout**: a control is "visible" whether it is on
its card, off it, or underneath something else.

`osy test --pixels` runs the **same tests** in a real browser instead. Nothing about your test file changes — the
same locators, the same refusals — but the geometric claims in [Layout assertions — is it actually usable on screen?](https://osysharp.com/reference/testing/ui-layout/) are actually judged, and
`Ui.Shot("label")` writes a PNG you can open.

```console
osy test                     # behaviour, in seconds
osy test --pixels            # the same tests, with layout checked and screenshots written
osy test --pixels --headed   # …and show the browser, slowed down, so you can watch it drive
```

It is opt-in because it costs a browser: the tier needs Playwright and a Chromium, or the Chrome already on your
machine, which it prefers. The check runs **before anything is compiled or booted**, so a machine that cannot run it
is told in a second — with the command that fixes it — rather than a minute into a run.

### The tests run exactly as they would anywhere   {#parity}

Local is not a weaker mode. Your app's own security is enforced just as it is in production: a `[Test]` runs as an
anonymous, secured caller, so a test that creates or reads data needs your model to grant it — a plain new app is
secure by default. This is the same behavior described in [Running tests](https://osysharp.com/reference/testing/running-tests/); the point of running locally is
speed and privacy, never a relaxed rulebook. The starter model a new project ships with grants exactly what its first
test needs and nothing more.

Everything else is identical to a remote run: each `[TestFixture]` seeds its own private branch, each `[Test]` forks
its own throwaway clone, results stream back one at a time, and nothing a test writes survives it. See
[Running tests](https://osysharp.com/reference/testing/running-tests/) for the full model.

### Choosing what to run   {#filtering}

The same two ways as a remote run:

- `--filter <text>` runs the tests whose names contain `text`.
- `--test <id>` runs exactly one test, named by its id (`file::fixture::name`). Repeat the flag for several.

A fixture is never filtered away, and a `[Skip("reason")]` test is always reported and never runs.

### Scripting a run   {#json}

`--json` writes one JSON object per line, in the order events happen, so a script can react to each test as it lands.
The command exits non-zero when any test fails or errors, and diagnostics go to standard error — standard output stays
a clean stream of events. The event shape is the same as a remote run.

### When there is no local platform   {#no-platform}

If no local platform is running for your project, `osy test` says so and stops, rather than silently reaching
elsewhere:

```console
No local platform is running for this project. Start one with `osyrin dev`.
```

Run `osyrin dev` and try again.

## Examples       {#examples}

The two-terminal loop — one platform, many test runs:

```console
# terminal 1
osyrin dev

# terminal 2
osy test
osy test --filter Totals
osy test --test "tests/orders.test.osy::Seeded::Totals_Add_Up"
```

A first test in a freshly scaffolded project, which passes because the starter model grants it:

```osy title="the model a fresh project scaffolds" test app=testing-running-tests-locally
// model/note.osy
entity Note {
  [Required] string Title;
  security { allow read, create when IsAuthenticated || IsAnonymous; }
}
```

```osy title="the first test that passes against it" test app=testing-running-tests-locally
// tests/note.test.osy
[Test]
void a_new_note_keeps_its_title() {
  var note = new Note { Title = "First note" };
  Assert.Equal("First note", note.Title);
}
```

## See also       {#see-also}

[Running tests](https://osysharp.com/reference/testing/running-tests/) — the same run against a remote platform, and the full description of fixtures, isolation,
and the streamed report.

[Debugging tests locally](https://osysharp.com/reference/testing/debugging-tests-locally/) — debug a single test locally with breakpoints and stepping in your editor.

[Running a local platform](https://osysharp.com/reference/local/running-a-local-platform/) — the local platform these tests run against, and how to start it.


---

<!-- https://osysharp.com/reference/testing/clock-advance/ -->

# TestClock.Advance

> In a test, moves the run's clock FORWARD by a duration, then lets the next Workflow.Settle fire every workflow timer the jump made due — reminders and instance/state deadlines.

<!-- id: testing-clock-advance · area: testing · stability: stable · html: https://osysharp.com/reference/testing/clock-advance/ -->

## Summary        {#summary}
`TestClock.Advance(delta)` moves the test's ambient clock forward by a `TimeSpan`, and it moves **every clock the app
reads**. An app reads time in three places, and a test that has said what time it is is believed by all three:

| where the time is read | does `Advance` move it? |
|---|---|
| the server, in memory — a function body | **yes** |
| the server, **inside a query** — `Item.Where(i => DateTime.UtcNow < i.Expires)` | **yes** — the platform's instant is bound into the SQL |
| the **browser** — a client expression or a `live var` computed over a list the client holds | **yes** |

So a row expiring in an hour is expired after `Advance(2h)` — in a function body, through a `Where(…)`, and on the
page — because they are three renderings of one question and a test may only get one answer to it.

⚑ **This matters most at a BOUNDARY, which is the only place it can bite.** "Is it due yet" is wrong for rows either
side of one instant and right for everything else, so a comparison off by an hour looks perfect until the hour that
decides it. Write the two tests that pin both sides of that instant — compute the distance to the boundary and step
one minute short of it, then one minute past — rather than one that advances a day and checks. Whether the second
crosses midnight otherwise depends on the hour the suite happens to run.

⚠ **In PRODUCTION a query's clock is still the database's `now()`.** Nothing here changes a deployed request, which
has no pinned clock; the binding happens only when a test has pinned one.

⚠ It is **TEST-ONLY, enforced by the compiler**: app code that moved its own clock would have no deadlines left, and
a milestone that breaches because the code said so is not an SLA. Workflow SLA clocks (reminders, the
per-state expire, the whole-instance deadline) are wall-time, so advancing the clock past a timer's due moment and
then calling <span class="planned" title="this page is planned and not written yet">Workflow.Settle</span> fires that timer deterministically — no real waiting.

## Signature      {#signature}
```osy syntax
TestClock.Advance(<TimeSpan>);   // e.g. TestClock.Advance(TimeSpan.FromHours(2))
```

## Description    {#description}
Time in a workflow SLA advances as wall-clock while a run waits. In a test that is not real time — you drive it.
`TestClock.Advance` adds `delta` to the run's clock (starting from the current instant, or now if the clock was never
pinned). The move alone changes nothing; the following `Workflow.Settle(entity)` runs the timer sweep, which fires
every clock now due:

- a **reminder** whose `After` (first fire) or `ThenEvery` (repeat) delay has elapsed runs its `Remind <Name>(…) { }` body and
  records a `Reminded` row on the [audit timeline](https://osysharp.com/reference/workflow/audit/) — a reminder never transitions the run;
- the instance **`Deadline`** or a state **`Expire`** whose budget has elapsed fires its `on Deadline` / `on Expire`
  route (which may `goto` a new state).

One `Settle` fires each due timer once; a recurring reminder fires once per `Settle`, matching how a periodic worker
sweeps. Because a reminder has no state change to observe, assert on the audit timeline (see [For(entity).Audit](https://osysharp.com/reference/workflow/audit/)).

The delta must be a `TimeSpan` — durations are always `TimeSpan.From…`, never a bare literal.

## Examples       {#examples}

⭐ **The commonest use, and the one this verb exists for: a state DERIVED FROM ELAPSED TIME.** Watering, renewals,
trials, overdue — anything where "has enough time passed?" is the app's whole question. Without `Advance` a test can
only back-date the seed, which never exercises the transition the user actually cares about.

```osy title="a lighthouse lamp falls due once its service interval passes" test app=testing-clock-due
entity Lamp {
  [MaxLength(80)] string Tower; DateTime LastServiced; int EveryDays = 7;
  security { allow read, create, update, delete when IsAnonymous || IsAuthenticated; }
}

[Page("/")]
[AllowAnonymous]
[Render(CSR)]
component Home() {
  live var lamps = Lamp.ToList();
  live var due = lamps.Where(l => (DateTime.UtcNow - l.LastServiced).Days >= l.EveryDays).ToList();
  render { Stack(gap: 2) { Text("Due now"); foreach (var l in due) { Text(l.Tower); } } }
}

[Test]
void a_lamp_falls_due_after_its_interval() {
  var l = new Lamp { Tower = "Skerryvore", LastServiced = DateTime.UtcNow, EveryDays = 7 };
  UnitOfWork.Commit();

  Ui.Visit("/");
  Assert.Hidden("Skerryvore");                 // just serviced — not due

  TestClock.Advance(TimeSpan.FromDays(8));     // the jump the keeper waits a week for
  Assert.Visible("Skerryvore");                // …and the CLIENT's live var sees it
}
```

⚑ That `due` is a **client** `live var` over rows the browser already holds — the third row of the table above —
and `Advance` moves it. This example is compiled and run by the docs gate, so it cannot go stale.

The workflow case, where the jump makes a TIMER due and the next `Settle` fires it:

```osy title="the workflow half — a jump makes a timer due, and the next Settle fires it" syntax
// A FRAGMENT, deliberately: it needs a workflow, its slot and its SLA clocks to stand around it. The compiled
// version of exactly this — advance, settle, assert on the timeline — is on [Remind (milestone reminders)](https://osysharp.com/reference/workflow/remind/).
var po = new PurchaseOrder { Title = "Laptops", Total = 20000 };
Workflow.Settle(po);                     // autostart → arms the slot's SLA clocks
PoApproval.RaiseSubmit(po);

TestClock.Advance(TimeSpan.FromHours(2));     // past the slot's Remind Nudge(After = 1h)
Workflow.Settle(po);                      // sweep fires the reminder

var audit = PoApproval.For(po).Audit;     // assert on the timeline, not on a state change
Assert.Equal(1, audit.Where(a => a.Kind == AuditKind.Reminded && a.Slot == "Legal").Count());
Assert.Equal(PoStatus.Review, po.Status); // a reminder does NOT transition — still waiting
```

## See also       {#see-also}
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — the timeline a fired reminder writes to
- [[Test] / [TestFixture]](https://osysharp.com/reference/testing/test/) — the `[Test]` a clock-driven assertion lives in


---

<!-- https://osysharp.com/reference/testing/index/ -->

# Testing (real app, real data, real rules)

> A test in Osy# is not a unit test with the world mocked out. It is your app, running against its own throwaway database, with your security rules switched on. That is what makes it worth writing — and it is why the one thing you must understand is who a test is ACTING AS: a test body runs secured, as an anonymous stranger, until you say otherwise.

<!-- id: testing-index · area: testing · stability: stable · html: https://osysharp.com/reference/testing/index/ -->

## Summary        {#summary}
A test is an ordinary function marked `[Test]`. It calls your real functions, against your real entities, with your
real security rules enforced — on **its own private copy of the database**, thrown away when it finishes.

There is nothing to mock, because there is nothing in the way:

```osy title="the model" test app=testing-index
entity Order {
  [Required, Unique, MaxLength(20)] string Code;
  decimal Total;
  invariant Total >= 0;

  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

void PlaceOrder(string code, decimal total) {
  var order = new Order { Code = code, Total = total };
}
```

```osy title="…and a test of it" run app=testing-index
[Test]
void Placing_an_order_stores_its_total() {
  PlaceOrder("A1", 42m);

  Assert.Equal(42m, Order.Single(o => o.Code == "A1").Total);
}
```

No repository to stub, no in-memory database to configure, no test double for your own model. You wrote the function;
the test calls it.

## Description    {#description}

### What a test actually runs against   {#isolation}
Before anything executes, the platform copies your app's database, compiles **your local source** and your tests into
that copy, and throws it away at the end. Nothing a test writes can outlive it, and nothing you have deployed can
mislead you — what runs is what is on your disk.

Inside that copy, the structure mirrors how you wrote the tests:

- a **`[TestFixture]`** builds the starting data **once**;
- **every `[Test]` that names it forks its own private clone** of that seeded state.

So two tests under the same fixture never see each other's writes, in any order, at the same time. That isolation is
the whole point: a suite where one test's leftovers change another's outcome is a suite that fails at random and then
gets ignored.

```osy title="a fixture seeds once; each test forks from it" run app=testing-index
[TestFixture]
void Seeded() {
  PlaceOrder("A1", 100m);
  PlaceOrder("A2", 50m);
}

[Test(Seeded)]
void The_seeded_orders_are_there() {
  Assert.Equal(2, Order.Count());
}

[Test(Seeded)]
void A_new_order_is_not_seen_by_its_siblings() {
  PlaceOrder("A3", 5m);

  Assert.Equal(3, Order.Count());   // its OWN clone: the seeded two, plus this one
}
```

The second test creates a third order and sees three. The first still sees two. Neither undoes anything.

### The one thing to understand: who is the test acting as?   {#acting-as}
**This is the paragraph that will save you an afternoon.** The fixture and the test body do not run under the same
rules, and the difference is deliberate:

| | Security | Why |
|---|---|---|
| **`[TestFixture]`** | **UNSECURED** | it is scaffolding. It seeds freely across every entity, including ones nobody is allowed to create, so that setting up a scenario never requires weakening a rule. |
| **`[Test]` body** | **SECURED, as an initially-anonymous principal** | it is the thing under test. Your rules are switched on, and nobody is signed in — so a denial is a *real* denial. |

Two consequences, and they explain almost every surprise a newcomer hits:

**1. A test that just calls a function may be denied — and that is the system working.** An app is
[deny-all by default](https://osysharp.com/reference/security/secure-by-default/), and a `[Test]` is a stranger:

```osy title="the model — an app with users, and a table only they may write" test app=testing-index-secured
[Principal]
entity User {
  [Required, MaxLength(60)] string Name;
  security { allow read where Id == user.Id; }
}

entity Ledger {
  [Required, MaxLength(80)] string Entry;
  security { allow create, read when IsAuthenticated; }
}

void Record(string entry) {
  var line = new Ledger { Entry = entry };   // no auth code — the rule does the work
}
```

```osy title="a stranger is refused — which is exactly what you want to be able to prove" run app=testing-index-secured
[Test]
void An_anonymous_caller_cannot_write_to_the_ledger() {
  Assert.Denied(() => Record("payroll"));

  Assert.Empty(Ledger.ToList());
}
```

**2. To test what a real user does, you must ACT AS one.** That is what [[testing-runas-attribute|`[runas]`]] is for,
and it is the only way security gets tested at all.

### Testing the rules — the part nobody else can do for you   {#security}
A security rule is the one kind of code whose bugs are invisible until they are catastrophic. A rule that is too
*strict* fails loudly the first time someone uses the app. A rule that is too *loose* fails silently, forever.

So: declare a **`principal`** — a name for a seeded `[Principal]` row — and run the test as them.

```osy title="the model, and its rules" test app=testing-index-rules
[Principal]
entity User {
  [Required, MaxLength(60)] string Name;
  security { allow read where Id == user.Id; }
}

entity Doc {
  [Required] User Owner;
  [MaxLength(200)] string Title;
  security { allow read where Owner == user; }   // you see your own documents. Nobody else's.
}
```

```osy title="…and the proof that they hold" run app=testing-index-rules
principal Alice => User.Single(u => u.Name == "Alice");
principal Bob   => User.Single(u => u.Name == "Bob");

[TestFixture]
void Seed() {
  // The fixture is UNSECURED, so it can seed rows that nobody is allowed to create.
  var alice = new User { Name = "Alice" };
  var bob   = new User { Name = "Bob" };
  var a = new Doc { Owner = alice, Title = "alice-doc" };
  var b = new Doc { Owner = bob,   Title = "bob-doc" };
}

[Test(Seed)]
[runas(Alice)]
void Alice_sees_her_own_document() {
  Assert.Single(Doc.ToList());
  Assert.Equal("alice-doc", Doc.Single().Title);
}

[Test(Seed)]
[runas(Bob)]
void Bob_cannot_see_Alices_document() {
  // Not "is hidden in the UI" — the row is not SELECTED. The rule is inside the query.
  Assert.Null(Doc.FirstOrDefault(d => d.Title == "alice-doc"));
}

[Test(Seed)]
void A_stranger_sees_nothing_at_all() {
  Assert.Empty(Doc.ToList());
}
```

Three tests, and between them they pin the rule from every side: the owner sees it, another user does not, and a
stranger sees nothing. `[runas]` **binds, never creates** — the selector must resolve to a row the fixture seeded, so
a denial test can never quietly pass against a principal production would never grant.

**A rule you have not tested is a rule you only believe you wrote.**

### What to assert   {#asserting}
The everyday assertions are equality and null checks. The two that earn their keep are the ones that prove a
*refusal*:

- **`Assert.Denied(() => …)`** — the acting principal is **refused** by a security rule. This is how you prove a rule
  bites.
- **`Assert.Throws<T>(() => …)`** — the code **faults**: your own [`throw`](https://osysharp.com/reference/function/throw/), or a broken
  [invariant](https://osysharp.com/reference/entity/invariants/) or [constraint](https://osysharp.com/reference/entity/constraints/) arriving as a `ValidationException`.

The distinction matters: `Denied` means *you were not allowed*, `Throws` means *it was not valid*. A test that
confuses them will pass for the wrong reason. See [Assert](https://osysharp.com/reference/testing/assert/) for the full set.

**Testing what the SCREEN does is [Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/)** — `Ui.Visit` opens a route, `Ui.Click`
presses what a person would press, and the same `Assert.*` verbs ask the questions. It is the same test, so nothing
on this page stops applying; `within:` is how you address one row when several read alike.

⛔ **And what the screen does is not the same question as whether a person can USE it.** Every assertion above is
about text or state, and all of them pass on a page that is visually broken — a button under an overlay, a label cut
off by its own box. [Layout assertions — is it actually usable on screen?](https://osysharp.com/reference/testing/ui-layout/) is the vocabulary for that, checked by `osy test --pixels` in a real
browser; under plain `osy test` those claims report themselves NOT CHECKED rather than green.

```osy title="proving the invariant is real, not decorative" run app=testing-index
[Test]
void An_order_cannot_have_a_negative_total() {
  Assert.Throws<ValidationException>(() => PlaceOrder("A9", -1m));

  Assert.Empty(Order.ToList());   // and the refused row was not left behind
}
```

### Why does an assert commit my writes?   {#asserts-settle}
Before every `Assert.*`, the platform **commits** whatever the test has written so far — which is why a test reads
real, stored rows and never needs a `UnitOfWork.Commit()` of its own.

It has one consequence worth knowing, because it will otherwise confuse you for half an hour: **an assert changes the
state the next line runs against.** If you are testing something that depends on work being *uncommitted* — how a query
treats a pending edit, say ([Querying data](https://osysharp.com/reference/query/index/)) — an assert placed before it will have settled that work, and you will
observe the committed behaviour instead.

So put the act you are testing **before** the assertions about it, and give each scenario its own `[Test]` (they fork
their own copies anyway, so this costs nothing).

### Parking a test you cannot write yet   {#skip}
`[Skip("reason")]` parks a `[Test]`, and the reason is **required**. Use it when the behaviour you want is not
expressible yet — the parked test is the executable statement of what you meant, and it surfaces as a real skip in the
run rather than vanishing. Deleting it would delete the only record that the gap exists.

### Running them   {#running}
`osy test` compiles your source and your tests together and runs them against a throwaway copy, streaming each result
as it finishes. Tests run only against a **Development** app — see [Running tests](https://osysharp.com/reference/testing/running-tests/) and
[Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/), and [Debugging tests locally](https://osysharp.com/reference/testing/debugging-tests-locally/) when one is failing and you want to stop
inside it.

## See also       {#see-also}
- [[Test] / [TestFixture]](https://osysharp.com/reference/testing/test/) — `[Test]` and `[TestFixture]`
- [Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/) — driving the app's UI from a test: `Ui.Visit`/`Ui.Click`/`Ui.Fill`, and `within:` when two rows read alike
- [Layout assertions — is it actually usable on screen?](https://osysharp.com/reference/testing/ui-layout/) — the GEOMETRIC claims: is it clickable, inside its card, cut off? — and `osy test --pixels`
- [Assert](https://osysharp.com/reference/testing/assert/) — the assertions, including `Assert.Throws` and `Assert.Denied`
- [[runas(Name)] test attribute and principal selectors](https://osysharp.com/reference/testing/runas-attribute/) — `principal` declarations and `[runas(Name)]`
- [runas](https://osysharp.com/reference/testing/runas/) — the `runas(…) { }` block, for two principals in one test
- [Outbound calls in a test](https://osysharp.com/reference/testing/outbound-calls/) — a `[Test]` reaches the network for real, which is the only way a suite can prove the endpoint works
- [Calling your own REST API from a test](https://osysharp.com/reference/testing/api-calls/) — the OTHER direction: calling a route your own app publishes, and asserting the status it answers
- [Running tests](https://osysharp.com/reference/testing/running-tests/) — `osy test`
- [The security model](https://osysharp.com/reference/security/index/) — the rules you are proving
- [Functions (the unit of work)](https://osysharp.com/reference/function/index/) — the functions you are testing


---

<!-- https://osysharp.com/reference/testing/ui/ -->

# Ui — drive the app's UI from a test

> Drive the real UI from a test: navigate to a route, click what a person would click, and assert on what the screen shows. `Ui.*` does things; `Assert.*` asks the questions — the same assertions a data test uses, so there is no second dialect to learn. A locator is CASE-SENSITIVE and matches by containment, with an exact match winning; two matches refuse. Say WHERE with `within:`, which takes EITHER an entity the test already holds — `Ui.Click("Edit", within: order)` presses that order's row — OR a string naming a container by its `label:`, so `Ui.Click("Save", within: "Billing")` presses the one inside the Billing card. Never rename a button, a card or a message to make a locator resolve — you almost never have to, because an exact match already beats a longer title that merely contains it: a `Button("Add")` inside a `Card("Add an expense")` is pressed by `Ui.Click("Add")`. When two things genuinely collide, say WHERE with `within:`.

<!-- id: testing-ui · area: testing · stability: preview · html: https://osysharp.com/reference/testing/ui/ -->

## Summary        {#summary}
A UI test is an ordinary `[Test]`. `Ui.Visit` opens a route and `Ui.Click` presses something; `Assert.Visible`,
`Assert.Hidden` and `Assert.OnPage` say what should be true of the screen afterwards. It runs the REAL page — the
same render, the same actions, the same security — so one test covers rendering, interaction and authority together.

A locator is **case-sensitive** and matches by **containment**, with an exact match winning over a substring. Two
matches **refuse**. Scope with `within:`, which takes **either of two things** — an **entity the test already
holds**, or a **string** naming a container by its `label:`:

```osy title="scope by the row, by a container, or by BOTH" syntax
Ui.Click("Edit", within: order);        // an ENTITY — that order's row, no title or row text to guess
Ui.Click("Save", within: "Billing");    // a STRING — the container whose `label:` reads Billing
Assert.Visible("Shipped", within: order);
Ui.Within("Needs watering") { Ui.Click("Water", within: plant); }   // BOTH — when the row is on screen twice
```

⛔ **Never rename a button, a card, a heading or a message to make a locator resolve — and you almost never have
to.** An exact match already beats a longer title that merely CONTAINS it, so `Button("Add")` sitting inside
`Card("Add an expense")` is pressed by `Ui.Click("Add")` with nothing added to either. When two things genuinely
collide, say WHERE with `within:`. See [[testing-ui#strict]] — read it BEFORE you name anything, not after a locator
has already refused.

## Signature      {#signature}
```osy syntax
Ui.Visit(path);            // navigate to a route and wait for it to settle
Ui.Fill(field, value);     // type into the field with this label (or placeholder/name)
Ui.Click(label);           // press the thing carrying this label — its ACCESSIBLE NAME, not only visible text
Ui.Click(label, within: order);      // …in THAT entity's row — an entity the test holds, no title needed
Ui.Click(label, within: "Billing");  // …or a container by its `label:` (a `Card("Billing")` title IS that label)
// ⚑ `within:` is NOT a Ui.Click parameter — EVERY verb and assertion that LOCATES something takes it, on
//    exactly these terms: Ui.Fill("Name", "Ada", within: dialog) · Assert.Visible(t, within: card) · Ui.Select
//    · Ui.Check/Uncheck · Ui.Upload · Ui.Hover · Assert.TextIs/Value/Enabled/Items/Before/Cell/… The only ones
//    that do not are the ones that locate NOTHING — Ui.Visit, Ui.Viewport, Ui.Press, Ui.Back/Forward,
//    Ui.SignInAs/SignOut (and Assert.OnPage / Assert.Dialog, which are page-level by construction).
Ui.Hover(control);         // move the pointer over this label, so `:hover` CSS applies — see [[testing-ui#hover]]
                           //  for the happy-dom-vs-`--pixels` split: it locates in both, but only `--pixels`
                           //  moves a real pointer.
Ui.Screen();               // PRINT the rendered page into the test output — asserts nothing, never fails
// ⚑ Reach for `Ui.Screen()` instead of asserting a string you know is absent purely to dump the page. It was
//    missing from THIS list until 2026-08-27 and an eval run therefore never found it: the verb existed, its
//    section was written, and the one place a reader actually reads was the one place it was not.
Ui.Check(field);           // put a checkbox ON  (never a toggle — order does not matter; it PRESSES it, so
                           //  the action behind it runs and the page re-renders before the next line)
Ui.Select(control, value); // choose in a dropdown/list — the VALUE it holds, never the label it draws
Ui.Viewport(1280);         // render at this width — and RESIZE to it, which is itself worth testing
Assert.Items(list, n);     // how many items a list is SHOWING (0 is a real answer, not "missing")
Assert.Before(a, b);       // a's row is rendered above b's — the assertion a SORT needs
Assert.Cell(row, column, expected);   // ONE cell of a table: the row by what it READS, the column by its header
Assert.Dialog(title);      // a modal is open, and this is its title (Assert.Visible cannot answer this)
Assert.Checked(field, on); // the read side of Ui.Check
Assert.Expanded(control, open);   // a disclosure control says whether it is open
Assert.Selected(option, chosen); // an option says whether it is the chosen one
Assert.Probe(control, field, expected);   // a FOREIGN control's own internal facts
Assert.Focused(control);   // what the keyboard is on — the other half of Ui.Press
Assert.Enabled(control);   // …and the authority half: is this control operable?
Assert.Disabled(control);
Assert.DisabledBecause(control, reason);   // refused, AND it says why — the app's own sentence
Ui.Uncheck(field);         // …and OFF
Ui.Press(key);             // a named key on the focused element: "Enter" · "Escape" · "Tab" · "ArrowDown"
Ui.SignInAs(principal);    // BE a declared principal — a real session, no login page
Ui.SignOut();              // end the session; the screen re-checks in place
Ui.AwaitChange(Order);     // wait for the live update another session's commit pushes to this page
Ui.Back();  Ui.Forward();  // the browser's history buttons — real history, not a re-visit
Ui.Upload(control, fileName, content);   // choose a file — the bytes really are posted
TestClock.Set(instant);    // pin the clock — the BROWSER's too, so a countdown is assertable
Assert.Violation(field);                   // this field is refused — the save was blocked BECAUSE OF IT
Assert.Violation(field, message);          // …and it says so (CONTAINS, not word for word)

Assert.Visible(text);      // the text is on the screen (CONTAINS — somewhere, in something)
Assert.Hidden(text);       // the text is not
Assert.TextIs(text);       // something reads EXACTLY this — "0" is not "10", "on time" is not "not on time"
Assert.OnPage(path);       // this is the route now showing
Assert.Value(field, v);    // what a FIELD holds — not what the page happens to print
Assert.Flow(container, direction);   // this container lays its children out "across" or "down"
```

## Description    {#description}
**It drives the real UI, not a model of it.** The page is rendered by the same client a browser runs, against the
same server and the same `security { }` rules. So a test can prove things no API test reaches: that a control is
absent for the wrong principal, that a form's submit landed somewhere, that a gated route sends a visitor to login.

**`Ui.*` acts, `Assert.*` asks.** That split is deliberate. Everything you already know about assertions applies
here, and a UI test reads like the data tests next to it rather than like a browser script.

**Waiting is not your job.** Each verb settles before returning — a click waits for the action it started to finish,
including the server round trip and the re-render that follows. Tests do not sleep, and there is no "flaky, add a
wait" step.

⚑ **A test owns its world, so there is nothing to race against.** It runs against its own branch off the compiled,
seeded app, with one client and one principal — nobody else is writing. So a verb's settle is the whole story, and
tests neither sleep nor retry.

**Say the back-and-forth out loud.** When a flow crosses the server and comes back, write the step that reads again
rather than assuming the screen caught up on its own. An explicit re-read is a sentence anyone can follow; an
invisible wait is a mechanism they have to trust.

**To be a signed-in user, SIGN IN.** A UI test authenticates by driving the app's own login form — `Ui.Visit("/login")`,
fill, click — which runs the real authentication pipeline, which is the thing worth testing anyway. A login is three
verbs, so wrap it in an ordinary function when you use it twice; there is no special helper to learn.

**Signing in signs the whole test in.** Once the app's login has taken, a row read beside the page is read as that
person — see [#signed-in](#signed-in). So "did the UI really write that?" is an ordinary read, with nothing to declare
twice.

⚠ **`runas` does NOT apply to the UI: a UI verb inside one does not compile.** `runas` is for being SOMEBODY ELSE, and
it rebinds the principal on the engine only — the browser is a separate session, still signed in as whoever it was. So
the page would render as THAT person while the test claimed to be this one. The refusal exists because the failure
inverts an assertion rather than merely losing one: `Assert.Hidden("someone else's order")` would pass against a page
showing exactly the wrong thing — the assertion whose whole job is proving data did not leak, green precisely when it
did. Assert the screen outside the block; reach the UI indirectly, through a helper called from inside it, and the run
refuses it there instead.

**What it does not do:** pixels. Colours, spacing, fonts and screenshots are not what this asserts — it tests what
rendered and how it behaved, not how it looked.

### What text does a locator match?        {#matching}

| Written | Matches |
|---|---|
| `Ui.Click("Save")` · `Ui.Fill` · `Ui.Select` · `Assert.Enabled` — anything that ACTS | the ACCESSIBLE NAME (a control's `label:`) first, then visible text. An EXACT match wins; otherwise CONTAINS |
| `Assert.Visible("Water")` · `Assert.Hidden("Water")` | CONTAINS — so it also matches `Water (due)` and `Waterfall` |
| `Assert.TextIs("Water")` | EXACT — one element reads precisely this. Whitespace is collapsed on both sides |
| `within: "Needs watering"` | CONTAINS — the container whose `label:` holds those words, or the row that reads them |
| `within: order` — an ENTITY the test holds | that entity's row, by identity. No title, no row text, no `label:` needed |
| every one of them | CASE-SENSITIVE. `Ui.Click("water")` does not press `Water` |
| two matches | REFUSED, naming both and where they sit. Add `within:` or `Ui.Within(…)` |
| the SAME row rendered in TWO containers | `within: <row>` alone is still two matches. Name the container first — `Ui.Within("Due now") { Ui.Click("Water", within: fern); }` — scopes COMPOSE, outer to inner |
| which verbs take `within:` | every one that LOCATES something, not `Ui.Click` alone. Not `Ui.Visit`/`Viewport`/`Press`/`Back`/`Forward`/`SignInAs`/`SignOut`, which locate nothing |

⛔ **A refusal means SAY WHERE, not RENAME.** Keep the button labelled `Water (due)`, the empty state reading
`Every plant is watered.` and the filter labelled `Room`; scope the locator instead.

| Instead of | Write |
|---|---|
| renaming a `Water (due)` button so `Assert.Hidden("Water")` passes | `Assert.Hidden("Water", within: plant)` |
| rewording an empty state to dodge a substring | `Assert.TextIs("Every plant is watered.")` |
| renaming a `Room` filter that collides with a `Room` column header | `Ui.Click("Room", within: "Filters")` |

```osy syntax
Card("Needs watering") { foreach (var p in due) { Row { Text(p.Name); Button("Water (due)", onPress: () => Water(p)); } } }

Ui.Click("Water (due)", within: fern);        // ✓ the fern's row — an entity the test holds
Assert.Hidden("Water (due)", within: cactus); // ✓ scoped, so another row saying it is not this row's business
```

## Examples       {#examples}
A page, and a test that opens it and says what should be on the screen:

```osy title="a page, and a test that opens it and reads the screen" test app=testing-ui-basics
[Principal] entity User {
  [Required, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

[Page("/welcome")]
[AllowAnonymous]
[Render(CSR)]
component WelcomePage() {
  render {
    Stack { Text("Welcome"); }
  }
}

[TestFixture]
void Seed() {
  new User { Email = "alice@test" };
}

[Test(Seed)]
void the_welcome_page_renders_and_is_where_we_landed() {
  Ui.Visit("/welcome");

  Assert.Visible("Welcome");
  Assert.Hidden("Sign out");        // nobody is signed in, so the shell's sign-out is not there
  Assert.OnPage("/welcome");
}
```

Signing in, and the helper that falls out of doing it twice — ordinary Osy#, no platform surface involved:

```osy title="signing in through the UI — an ordinary helper you write" syntax
void SignIn(string email, string password) {
  Ui.Visit("/login");
  Ui.Fill("email", email);
  Ui.Fill("password", password);
  Ui.Click("Sign in");
}

[Test(Seed)]
void alice_sees_only_her_orders() {
  SignIn("alice@acme.test", "hunter2");

  Ui.Visit("/orders");
  Assert.Visible("Order #1001");        // hers
  Assert.Hidden("Order #2002");         // Bob's — genuinely not rendered, not merely hidden
}

[Test(Seed)]
void an_anonymous_visitor_is_sent_to_the_login_page() {
  Ui.Visit("/orders");
  Assert.OnPage("/login");
}
```

## Signing in, versus `runas`        {#authenticating}
Two ways to be somebody in a test, and they are not interchangeable.

| | `runas(Alice) { … }` | signing in through the UI |
|---|---|---|
| what it binds | the ENGINE's acting principal | the BROWSER's session |
| exercises auth | no — it imposes the answer | yes — the real `[AuthMethod]` pipeline |
| use it for | data, function, workflow and security tests | anything that renders |
| with `Ui.*` | **does not compile** | the supported way |

**A UI verb inside `runas` is refused, and that is a security property rather than a limitation.** The page is
rendered by a client with its own session, so a `runas` block cannot reach it — the page would render anonymously
while the test claimed to be Alice. Left silent, that inverts an assertion: an anonymous visit to a gated route
renders nothing, so `Assert.Hidden("someone else's order")` would pass on an empty page, and the assertion whose
whole job is proving data did not leak would be green exactly when the page is broken.

You find this out at COMPILE time — a `Ui.*` verb or a `Assert.Visible` / `Assert.Hidden` / `Assert.OnPage` written
inside a `runas` block is an error naming the reason, so the fix is a keystroke rather than a puzzling run. Call a
helper that drives the UI from inside the block and the compiler cannot see it; that one is refused when the verb
actually runs. Both answers say the same thing, and neither is a warning.

So a UI test signs in the way a person does. That costs three verbs, which you wrap in a function the second time
you need it — see the example above.

## Does the page see what the test just wrote?        {#seeding}
**Yes, and you need nothing to make it so.** `Ui.Visit` — and every other `Ui.*` verb — settles the test's pending
writes before it drives anything, so the page reads them like any other stored row. No `[TestFixture]`, no
`UnitOfWork.Commit()`, and no assertion in between. A fixture is for SHARING a seed across several tests, not for
making a seed take effect.

```osy syntax title="a seed written in the test body"
[Test]
void a_row_made_here_is_on_the_page() {
  new Job { Title = "Skim the ceiling", SortOrder = 5 };   // no fixture, no explicit commit
  Ui.Visit("/");
  Assert.Visible("Skim the ceiling");                      // the page sees it
}
```

⛔ **So do not write an assertion whose only job is to force a commit.** `Assert.Equal(1, Job.Count());` slipped
between the `new` and the `Ui.Visit` proves nothing, and it is the shape people arrive at when they are unsure
whether the write has landed. It has landed.

## Does a query see what the page has not saved yet?        {#saving}
That is the OTHER direction — what a query in the test sees of what the PAGE is holding — and it has a real
boundary, which the rest of this section is about.

A page holds its edits in a unit of work until something commits them, which is what makes "Save" mean anything. A
test sees that boundary exactly as the database does:

- Before the app's save runs, the edits are on the SCREEN and not in the database. A query in the test finds nothing.
- After it runs, the query finds the row.

That is the sharpest test available of a page that must not write early — assert the absence first, then act, then
assert the row. Both halves are ordinary Osy#: `Ui.*` drives, and the query is the same query any other test writes.

**Which read you are looking at decides what it can see, and there are three of them.** The test's own query is the
easy one — it is a different reader from the page, so it sees the database and nothing of the page's pending work.
Inside the page the answer depends on **where the read is written**:

| the read | where it runs | what it sees |
|---|---|---|
| a query **in the test** | the test's own reader | the database — never the page's unsaved edits |
| a **field of a row the page already holds** (`order.Code`), read anywhere — render, a `live var`, or inside an action | in the browser, over the page's pending edits | the pending edit, before any save |
| a **`live var` list query** | in the browser | the fetched rows with pending edits applied, plus rows this page created and has not saved |

⚠ **A query WRITTEN INSIDE AN ACTION is a fourth case, and it is not one to build on yet.** It is a server round
trip taken at that point in the body, so its membership and any `Count`/`Sum` it folds are computed from stored
values, not from what the page is holding. Whether that is the intended rule is an open question the platform still
owes an answer — so do not write a test that depends on either answer. Read a `live var` instead, or commit first
and then query.

```osy test app=testing-ui-saving title="what a test query sees of a page's unsaved edits"
[Principal] entity User {
  [Required, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Order {
  [Required, MaxLength(50)] string Reference;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

[Page("/orders/new")]
[AllowAnonymous]
[Render(CSR)]
component NewOrder() {
  string reference = "";

  // The page's OWN Save — a button a person presses, not anything the test framework supplied.
  action Save() {
    new Order { Reference = reference };
    UnitOfWork.Commit();
  }

  render {
    Stack {
      Input(value: reference, placeholder: "Reference");
      Pressable(onClick: Save) { Text("Save"); }
    }
  }
}

[Test]
void editing_does_not_write_until_save() {
  Ui.Visit("/orders/new");
  Ui.Fill("Reference", "PO-9001");

  Assert.Equal(0, Order.Count());        // on screen, not in the database

  Ui.Click("Save");

  var saved = Order.Single(o => o.Reference == "PO-9001");   // now it is — an ORDINARY query
  Assert.Equal("PO-9001", saved.Reference);
}
```

## What does the screen actually say? — `Ui.Screen()`        {#screen}
`Ui.Screen()` prints the rendered page — its route and every line of text on it — into the test's output. It asserts
nothing and never fails:

```osy syntax
Ui.Click("Add an expense");
Ui.Screen();                      // ← what is actually on screen now
Assert.Visible("What was it for");
```

```text
    │ Ui.Screen() — /add
    │   Add an expense
    │   What was it for
    │   How much
  ✓ the_front_page_leads_to_the_add_screen (1674ms)
```

`Log.Information(…)` in a test prints the same way, so a value and a screen read alike.

⚑ **Reach for it INSTEAD of asserting something you know is absent.** A failing `Assert.Visible` prints the page in
its message, and that made a deliberately-failed assertion the only way to see a screen — an eval run wrote fourteen
versions of one probe test asserting `"__dump__"` to do it, and recorded the trick as the lesson of the whole run.
It is not a trick any more; it is a verb.

⚠ **A page dump is a LEAD, not a measurement.** Reading it tells you what to assert; the assertion is still what
proves the behaviour. And a `Ui.Screen()` left in a committed test is noise for whoever reads the run next — take it
out with the rest of the scaffolding.

## Who waits — the VERB, never the assertion        {#waiting}
Every verb finishes what it started before it returns. `Ui.Click` waits for the action it fired — including a
durable one, across the hand-off gap where the network genuinely goes quiet — and, when that action navigated, for
the page it navigated **to**: its `on mount`, its first paint, its live subscriptions. `Ui.Visit` does the same for
a fresh page, and `Ui.Fill` for an `onInput` that reaches the server.

**Assertions do not retry — they judge a SETTLED screen.** `Assert.Visible`, `Assert.Hidden` and `Assert.TextIs`
read the screen and, when it already says what you claimed, answer at once: that first read is the only cost a
passing test pays. When it does not, the assertion is not yet a verdict — it is a screen the client may still be
working on — so the harness waits, on evidence rather than on a timer, for the client to have **nothing
outstanding**: no action in flight, no navigation resolving, no list flush owed, no refetch a change signal has
scheduled but not yet issued, nothing on the wire. Then one short grace for a signal already on its way, and the
verdict. A retry on a guessed interval would turn a missing wait into a slow pass; this returns the moment the client
says it is finished and never asks a finished page to change its mind. `Assert.OnPage` and `Assert.Value` read once,
after the verb's own wait.

⚠ **So a failing assertion is a claim about the screen, not about timing** — and the page it prints is the page it
judged, from the same read. If the text then arrives late anyway, the failure says so in its own words:

```text
Assert.Visible("someone@spendflow.test") failed: 'someone@spendflow.test' is not on the screen. The page shows: …
⚠ AND IT IS ON THE SCREEN NOW: 'someone@spendflow.test' arrived AFTER this assertion was judged — the page went
quiet, the harness read it, and the update landed later than the settled-screen wait allowed. …
```

That is a **slow** update, not a missing one — something finishing after the click's own round trip (a workflow
step, a background run, another session), or a heavily loaded machine — and the sentence to write is
[`Ui.AwaitChange`](#await-change). A wait that ran out with the client still busy says so too (`AND THE HARNESS GAVE
UP WAITING`), which is a page that genuinely never settles, and THAT is the defect to chase.

⚑ **The one thing a verb cannot wait for is a change it did not cause** — another session's commit, a workflow, a
server function running elsewhere. That is what [`Ui.AwaitChange`](#await-change) is for, and it is the only wait you
ever write by hand.

## Waiting for a change somebody ELSE made        {#await-change}
A `live var` re-reads when the data it queries changes, including when the change came from another session
entirely — the other desk, a server function, a workflow. That push arrives on the **server's** schedule, so an
assertion written straight after the other session's commit races it, and the driver reads the screen as of its
last verb. `Ui.AwaitChange` is the sentence that says the back-and-forth out loud:

```osy syntax
runas (Priya) { TakeOrder(id); }      // the other desk acts, at her own screen

Ui.AwaitChange(Order);                // wait for the server's change signal, and the refetch it triggers

Ui.Within("Waiting to be picked up") { Assert.Hidden("Mr Halloran"); }
```

The operand is the **entity type** — what the server addresses its change frames to — never a row and never a
string. `Ui.AwaitChange(Order)`, not `Ui.AwaitChange("Order")` and not `Ui.AwaitChange(order)`.

It is **explicit on purpose**, and it is a real wait rather than a pause: if no signal arrives it fails, naming
the socket state, what this page subscribed to, and what signals it did see —

```text
no change signal for 'Order' arrived within 5000ms. Socket: open.
Subscribed to: metadata, Order, User. Signals seen: (none).
Either the server emitted nothing for this commit, or this page never subscribed — a query only subscribes when
it is declared `live`, and only an app-scoped (signed-in) session opens a socket at all.
```

⚠ **Each call consumes one signal for that type**, so two waits mean two updates rather than one update counted
twice. A signal that arrived before the call still counts: the write happens first and the signal chases it.

⚠ **You do not need it for a change this page made itself.** A click that commits is already settled by the verb
that made it. `Ui.AwaitChange` is for a commit that happened somewhere else.

## Ticking a box, pressing a key, signing out, reading a field        {#verbs}
Beyond `Visit` / `Fill` / `Click`:

```osy title="the other verbs" syntax
Ui.Check(field);  Ui.Uncheck(field);   // put a checkbox in a state — never a toggle, so order does not matter
Ui.Press("Enter");                     // a named key on the focused element: "Enter" · "Escape" · "Tab" · "ArrowDown"
Ui.SignOut();                           // end the session; the screen re-checks in place
Assert.Value("Reference", "PO-9001");  // what a FIELD holds — not what the page happens to print
```

`Assert.Value` asks the field, which is the point of having it: `Assert.Visible("PO-9001")` would also pass on a page
that merely printed the reference somewhere, and a pre-fill assertion must not accept that.

⛔ **`Ui.Check` PRESSES the control, so its action runs and the page re-renders — and on a filtered list that can
take the row off the screen.** Ticking a job on a page whose default view is "what's left" is exactly this: the
tick is right, the filter is right, and the row is correctly gone. So do not read its state afterwards — assert
what you expected:

```osy title="a tick that removes its own row" syntax
Ui.Check("Fix the squeaky stair");
Assert.Checked("Fix the squeaky stair", true);   // ✗ the row left the view — there is nothing to read
Assert.Hidden("Fix the squeaky stair");          // ✓ that IS the behaviour under test
Assert.Equal(true, Job.Single(j => j.Title == "Fix the squeaky stair").IsDone);   // …and the data says the rest
```

If a locator fails on a label your own previous verb removed, the driver says so and names the verb — but the
assertion above is the one to write in the first place.

**`Ui.SignOut` asserts in place.** Sign out while on a page that needed the session, then assert — without navigating
first. A navigation re-runs the route gate by itself, so a test that moved first would pass against a sign-out that
did nothing but forget a token. What you want to know is that the screen in front of the person changed.

### What size is the screen?        {#viewport}

```osy syntax
Ui.Viewport(1280);            // run at desktop width…
Ui.Visit("/orders");
Assert.Enabled("Name");       // …where the grid is a table with clickable headers

Ui.Viewport(390);             // now shrink — and the page must RE-FLOW
Assert.Items("Orders", 3);    // the rows are still there, laid out differently
```

Tests run at **1280 by default** — desktop, because that is the layout most of an app's users see. A test that wants
the phone says so.

**One verb, two uses, because they are the same act.** Before a `Ui.Visit` it chooses the width the page renders at.
After one it RESIZES — and that is worth testing in its own right: a responsive component has to re-flow when the
window changes, and an observer that never fires is a real bug that only a resize catches.

The width is **sticky across navigations**. A `Ui.Visit` re-opens the page, and a width that reset each time would
let a test set it, navigate, and silently go back to the default.

⚠ **Headless has no layout engine, so every container is reported as viewport-wide.** A component asks how much room
IT has (`Layout.AtLeast`), and a real sidebar's container is narrower than the window — here it is not. So a test
asserts the layout the app picks AT THAT WIDTH, which is the question a responsive test is asking; it is not a
substitute for looking at a real browser once.

### Hovering — CSS-only affordances        {#hover}

`Ui.Hover(control)` locates the element this label names — the same rule `Ui.Click` resolves by — and moves the
pointer over it, so `:hover` CSS applies. It composes with `within:` exactly as `Ui.Click` does, and it leaves the
pointer where it put it until the next locating verb moves it — a `Ui.Click` right after already moves the pointer
to ITS OWN target before it presses, so nothing has to be said to clear a stale hover:

```osy syntax
Ui.Hover("Options");                    // a tooltip, an icon-rail label, a chart's hover readout
Assert.Visible("Delete this board");    // the CSS-only text a hover reveals
```

⚠ **Plain `osy test` LOCATES but cannot HOVER.** happy-dom has no compositor and no pointer at all — `:hover` is
real browser input-device state that no DOM API can set, real or fake — so a typo in the label is still a real
error there, but the hover itself never claims to have happened. Nothing fails; the run carries a note instead:

```text
Ui.Hover("Options") — NOT CHECKED: this run renders in happy-dom, which has no compositor and no pointer —
`:hover` is real browser input state that no DOM API can set. Run the same tests with `osy test --pixels` to
drive a real browser and move a real pointer.
```

**`osy test --pixels` moves a REAL pointer**, through the browser's own input dispatch — the same mechanism a
person's mouse uses — so `:hover` genuinely matches and a CSS-only reveal is provably there or provably not. That
is also what makes an overlay built specifically to CATCH the pointer (a chart's invisible hover band drawn over
its marks) resolve correctly: the browser's own hit-test decides what is actually topmost at that position, exactly
as it would for a person.

**And it is the same real pointer an `onPointerEnter`/`onPointerLeave` action needs.** [pointer](https://osysharp.com/reference/ui/pointer/) is the half of
hover an app can ACT on; `Ui.Hover` under `--pixels` is what proves that action actually runs, the same way
`Ui.Click` proves an `onPress`. Under plain `osy test` neither the style nor the action can be checked — both need
a real pointer, and only `--pixels` has one.

### How are UI tests run, and why was mine SKIPPED?        {#no-driver}

**`osy test` runs your UI tests.** There is no separate command and no flag: a `[Test]` that drives the UI is run by
the same command, in the same run, beside the ones that do not. Each one renders the app through the real client,
against its own throwaway copy of the data — so what it asserts is what a person would see.

A UI test needs a JS runtime (node) to drive that client. On a box without one, a test that drives the UI is
**SKIPPED with the reason** — before its fixture is copied and before a single verb runs:

```text
- drives_the_ui  SKIPPED
    driving an app's UI runs the real client, and this machine has no `node` on PATH.
    Install node 22 or newer and run the tests again.
```

The rest of the suite is unaffected: a data test beside it runs normally, and nothing is disabled on a machine that
CAN run the client.

**`node` is the only requirement** — there is nothing to install and nothing to configure. The driver ships with the
platform as a single self-contained file, and the client it drives is the one your own running app is already
serving, so a UI test works the same in a fresh install as it does anywhere else.

⚑ **The platform works out which tests those are, including through your helpers.** Signing in through the UI is
three verbs, so it belongs in a helper — and a test calling that helper drives the UI just as much as one spelling
the verbs inline. Nothing is declared and nothing is annotated.

### Signing in signs the WHOLE test in        {#signed-in}

```osy syntax
SignIn("ada@acme.test", "hunter2");     // your own helper: visit /login, fill, click

Ui.Visit("/orders/new");
Ui.Fill("Reference", "PO-9001");
Ui.Click("Save");

var saved = Order.Single(o => o.Reference == "PO-9001");   // read AS Ada — no second declaration of who she is
Assert.Equal("ada@acme.test", saved.Owner.Email);
```

From the moment the app's own login takes, the test body **is** that person: a row read beside the page is read with
that principal's permissions. So checking that what the UI did really landed in the database is an ordinary read.

**`runas(P)` is for being SOMEBODY ELSE** — proving a row is invisible to another user, or that a second person
cannot close the first person's issue. It still cannot enclose a `Ui.*` verb or a UI assertion: the browser is a
separate process and is still signed in as whoever it was, so the page would render as THAT person while the test
claims to be this one. Assert the screen outside the block; to drive the UI as someone else, sign in as them.

### What order are they in?        {#order}

```osy title="the assertion a sort needs" syntax
Assert.Before(alba, zeno);       // alba's row is rendered above zeno's
Ui.Click("Who");                 // sort by that column the other way
Assert.Before(zeno, alba);
```

The assertion a SORT needs. `Before` rather than a whole-list `Order` because *"now Zeno comes before Alba"* is the
sentence a person says, and it does not make a test restate every row it did not care about.

Both operands are **values**, matched on the row identity the platform stamped as it rendered — never on rendered
text, which a cell template can change and which two rows can share. So an entity works as readily as a string.

⚠ **A row shown twice is REFUSED, not resolved.** The same value rendered in two lists has no single position, so
comparing it would answer a question nobody asked — quietly, and differently depending on which list rendered first.
A failure shows the order that WAS rendered, which is usually the whole diagnosis.

⛔ **A STRING NAMES A ROW BY ALL OF WHAT ITS FIRST CELL READS — not by the part you care about.** A row rendering a
name, a category, a number and two buttons is named `"Cellared Riesling White 12 ▲ ▼"`, so the obvious
`Assert.Before("Cellared Riesling", …)` matches nothing:

```osy title="naming a row: by entity, not by its text" syntax
Assert.Before("Cellared Riesling", "Corked Merlot");                            // ✗ no row reads exactly that
Assert.Before(Bottle.Single(b => b.Name == "Cellared Riesling"), corkedMerlot);  // ✓ matched by identity
```

**Pass the row's own entity.** It matches by identity, so it does not move when the row is restyled, gains a column
or has a button added — which a string does. Naming the row in full works and is the brittle option.

### How many is it showing?        {#items}

```osy syntax
Assert.Items("Orders", 3);
Assert.Items("Archive", 0);   // present and EMPTY — which is a real answer
```

Without it a list page is assertable only by "some text is on the screen", which passes on a page showing one row
and on a page showing a hundred.

**It counts the platform's own row stamp**, not a shape guessed at from the markup. Every row a `foreach` renders is
stamped as it is rendered, so the count survives a restyle, an extra wrapper and a change of atom — none of which a
selector-shaped count would. A row that renders several sibling elements still counts once.

⚠ **Zero and absent are different answers, and stay different.** A list that is on the page and empty counts 0 —
asserting that is how you prove a filter excluded everything. A list that is not there at all is a failure naming
what the page DOES show. An assertion that conflated them would report a page which failed to render as a
successful empty filter, and that is the direction you most want to hear about.

The list is located the way everything else is: by what a person would call it. Give the container a name —
`Stack(role: UiRole.List, label: "Orders")` — which is the same declaration that makes it navigable to a screen reader
(see [accessibility](https://osysharp.com/reference/ui/accessibility/)).

### Why won't it save?        {#validation}

```osy syntax
Ui.Fill("code", "FAR-TOO-LONG-FOR-THIS");
Ui.Click("Save");

Assert.Violation("Code");                              // refused — and the save was blocked because of it
Assert.Violation("Code", "at most 8 characters");      // …and this is what it says
Assert.Empty(Contact);                                 // …and nothing was written
```

**It asks the MODEL, not the page.** A violation carries the entity and field it is about, so "which field is this
message under" is answered by the data rather than by guessing at which text sits nearest which input.

**And it requires the message to be ON SCREEN.** Both halves, always — because either alone passes for the wrong
reason. The model alone goes green on a form that validates perfectly and shows the person nothing; the screen alone
is `Assert.Visible`, which passes when those words are anywhere on the page, including under a different field.

**The message is matched by CONTAINS, and it is optional.** A declared sentence carries the detail that makes it
useful, and a test that had to restate it word for word would be asserting the platform's wording rather than the
app's behaviour. `Assert.Violation("Code")` alone says "the save was blocked because of this field", which is often
the whole assertion.

⚠ **A violation is REVEALED, not merely computed** — it appears once the person has left the field or pressed save,
the same moment the browser reveals `:user-invalid`. A blank field must not be accused before anyone has filled it
in, so assert after the interaction, not before.

⚠ **A required field NOBODY HAS TOUCHED is refused by the SERVER, not here.** The page judges the edits it has; a
field never typed into is not among them. Catch `ValidationException` around your commit and show `ex.Message` — the
demos do — and assert that sentence with `Assert.Visible`.

### A composite `[Unique(A, B)]` — aim at the pair, not at a member {#composite-unique}

A [[entity-constraints#unique|composite unique]] refusal attaches to **one field whose name is the members joined
with `", "`, in the order they were declared**. So `[Unique(Expense, Person)]` is asserted as:

```osy syntax
Assert.Violation("Expense, Person");                                   // the pair collided
Assert.Violation("Expense, Person", "already on this expense");        // …and this is what it says
Assert.Violation("Share.Expense, Person");                             // qualified, when two entities share member names
```

`Assert.Violation("Expense")` finds **nothing**: there is no violation on either member alone. The constraint is
about the combination, so the thing refused is the combination, and it is named as one.

⚠ **It exists only AFTER a save the server refused.** `[Unique]` is a question about *other rows*, which the page
cannot answer, so — unlike `MaxLength` or `Required` — nothing is revealed by leaving the field. Press Save first;
the server's refusal is then held on the page beside the right control, exactly like a locally-caught one.

⚠ **Assert the MESSAGE only if you declared one.** `Assert.Violation(field)` alone always works. The two-argument
form needs the sentence to be on screen, and an *undeclared* composite unique is refused with a generated sentence
that names the entity and its columns — schema, so it is masked to "One or more values are invalid." for a caller
with no account. Write `[Unique(Expense, Person, "They are already on this expense.")]` and that sentence is what
both the person and the assertion get.

### What time is it?        {#clock}

```osy syntax
TestClock.Set(new DateTime(2031, 3, 4, 5, 6, 7));
Assert.Visible("2031-03-04");     // the page moved with it
```

**`TestClock.Set` pins the browser too, not just the engine.** It always pinned the server's clock; now the page a UI
test is driving reads the same instant. So a countdown, a "3 days left" badge, an "expires in" — anything a render
builds on `DateTime.UtcNow` — can be proved without a test waiting for real time to pass.

One verb and one instant, deliberately: a test that pinned the server to Tuesday and left the page showing the real
Friday would describe a world nobody can reason about.

**It sticks across navigation**, like the viewport. Pin it once and every later screen is on that clock.

⛔ **It is for asserting what the clock RENDERS — never for setting up an input you could not fill.** Pinning the
clock so a date field's *default* becomes the value under test looks like it works, and produces a test that never
touches the thing it claims to. Nothing you wrote is in the assertion: the default is the page's choice, so the
test passes for as long as that choice happens to agree with the pin and goes red on a change no reader will
connect to it. If a control resists driving, that is a defect worth reporting — reach for `Ui.Fill` on the field,
or set the value through the app's own function and assert what the page shows.

### How do I test an upload?        {#upload}

```osy syntax
Ui.Upload("Choose a file", "notes.md", "# Notes");
```

**The file is described, not read off a disk.** A test runs against a throwaway branch of a compiled app and has no
filesystem to point at, so a path would be a promise the language cannot keep. A name and its content are both things
the test already knows — and the name carries what most apps actually branch on, the extension.

**Only the dialog is simulated.** No code can open a file picker. Everything after the pick is the product: the bytes
are posted to the app's file store over the session, and your `onUploaded` action runs with the stored file — its
`FileName`, `ContentType`, `Length` and the `Path` the store wrote.

⚠ **An upload rides the SESSION, so sign in first.** An anonymous page cannot post to the store, which shows up as an
upload that never reaches your action.

### How do I test the Back button?        {#history}

```osy syntax
Ui.Visit("/orders");
Ui.Click("PO-1042");     // the app navigates
Ui.Back();               // …and the person presses Back
Assert.OnPage("/orders");
Ui.Forward();
```

**It is real history, not a re-visit.** Going back to a page the app navigated to is a `popstate`, and your ROUTER
has to answer it: match the previous route, decide it may be entered, re-render it with no page load. Re-visiting the
path would prove the route renders — which your other tests already prove — and leave the one path a back button
exercises untested. That path is also the one that rots quietly, because nobody presses Back while developing.

Going back across a `Ui.Visit` is a page load instead, exactly as in a browser. Same verb either way: a person
pressing Back does not know which kind of navigation brought them there, and neither should the test.

⚠ **Stepping off either end REFUSES.** Standing still would leave the previous screen on display, and every
assertion after it would pass against a page the test never navigated to.

### One cell of a table        {#cell}

```osy title="name the row by its first column, or pass the row itself" syntax
Assert.Cell("Alba Ruiz", "When", "2026-08-12");   // the row that READS this, under that column header
Assert.Cell(order, "Total", "£240.00");           // …or the row itself, when the test is holding one
```

**Name a row the way you would say it out loud** — "the Alba Ruiz row" — which is what its FIRST COLUMN reads. That is
the column a table uses to identify its rows to anyone looking at it, and it is all a test needs to talk about a row
it has only ever seen on screen.

Pass the row VALUE instead when the test already has one (a fixture seeded it, or you just read it back). Both work,
and they cannot disagree: an id is not something a cell renders.

**The column is named by its HEADER.** Never by position, and the cell is never found by the text it renders — that
text is the thing being checked, so finding the cell by it would make every assertion either pass or say "not found",
and never "reads the wrong thing".

⚠ **Two rows reading alike REFUSE**, like every other locator here. Narrow with `within:`, or pass the row itself.

Without it, a table's most interesting assertion is `Assert.Visible("£240.00")`, which passes when *any* cell
anywhere on the page says that — including the row you were proving had NOT changed.

**It reads the table the way a screen reader does**, and that is the design rather than a coincidence: both need the
table to declare its rows, headers and cells. So a grid this can address is a grid a person using a reader can
navigate column by column, and a grid it cannot is broken for both.

```osy title="what a hand-built table must declare to be addressable" syntax
Stack(role: UiRole.Grid) {
  Row(role: UiRole.GridRow) { Row(role: UiRole.ColumnHeader) { Text("Who"); }  Row(role: UiRole.ColumnHeader) { Text("When"); } }
  foreach (var p in people) {
    Row(role: UiRole.GridRow) { Row(role: UiRole.GridCell) { Text(p.Name); }  Row(role: UiRole.GridCell) { Text(p.Joined); } }
  }
}
```

The kit's own `DataGrid` declares all of this, so a grid built from it needs nothing extra. A hand-built table that
does not is REFUSED, naming the line that fixes it — rather than guessing at a position and asserting a plausible,
wrong cell.

⚠ **At a narrow width the kit's grid is a CARD LIST, and a card has no columns.** The failure says so, because the
fix is `Ui.Viewport(1280)` and not a different locator. A test that never says a width runs at the default — see
[#viewport](#viewport).

⚠ **A row with fewer cells than the header has columns is reported as the rendering defect it is**, not as an empty
cell. On screen, every value after the gap sits under the wrong heading.

### Choosing in a dropdown        {#select}

```osy syntax
Ui.Select("Status", OrderStatus.Shipped);   // an enum member
Ui.Select("Owner", alice);                  // an entity the test already holds
```

**The second operand is the VALUE the field holds, never the label an option draws.** Two people can share a name,
and a label is a presentation choice a template can change without changing what the field means — so a test written
against the label breaks on a restyle and silently picks the wrong row the day two options read alike.

That is not merely a preference: options are drawn by the CALLER's template. A dropdown over your own type renders
whatever you gave it — an avatar and two lines, possibly no text at all — so there may be no label to match on. The
platform stamps each row's identity as it renders it, and `Ui.Select` matches on that.

It **opens the control first** if it is closed, because that is what a person does and because a closed dropdown has
no options on the page to match against.

⚠ **A value with no stable identity is REFUSED.** An entity has one (its row), an enum member has one, a string or a
number is its own; a plain class instance is not any of those, so there is nothing to match — and falling back to the
rendered text would be the silent wrong answer this whole surface exists to avoid.

⚠ **An identity nothing on screen carries is a failure naming what WAS offered.** Selecting nothing quietly would
make a mis-typed test green against a dropdown that never moved.

It searches for the option **inside the control it opened**, not across the page. That matters because a row
identity is not unique: an enum's members are stamped wherever they render, so a `Tabs` row over the same enum
carries the same identities as the dropdown's options and would otherwise collide with them.

### Typing into a control that is not a plain text box        {#fill-controls}
`Ui.Fill` addresses a control by its **label**, and every labelled input answers to it — including the ones that do
not hold text:

```osy title="a number or a decimal is still typed as text — there is no numeric verb" syntax
Ui.Fill("How often", "10");        // a NumberField — the value is written as text, stored as an int
Ui.Fill("Price", "12.50");         // a DecimalField
Ui.Fill("Notes", "Water sparingly");
```

A numeric field is still a field: the string is what a person types, and the control parses it exactly as it does
when a person types it. There is no separate numeric verb, and none is needed.

A **date, time or date-and-time picker** is filled the same way, in **ISO** — and it is worth knowing what that
costs, because these controls render no text box at all. Each is a button over a month calendar or a pair of time
columns, so the fill *opens* it, walks the calendar to the right month, and clicks the day, the hour and the minute.
The value lands the way a person's click lands it, and the panel is shut again afterwards.

```osy title="a picker has no text box — fill it in ISO and the driver clicks its calendar for you" syntax
Ui.Fill("Due", "2026-08-23");             // a DatePicker
Ui.Fill("Opens", "09:30");                // a TimePicker — 24-hour, whatever the closed field reads
Ui.Fill("Starts", "2026-08-23 09:30");    // a DateTimePicker — BOTH halves are required
```

ISO is the spelling because it is the only one that is not a **presentation** choice: one screen shows the same date
three ways — `23 Aug 2026` on the closed field, `23 August 2026` on the day cell, `2026-08-23` in the app's own text —
and a test written against any of those breaks when a `culture` changes something the app does not care about.

⚑ **AND THE SAME ANXIETY ABOUT NUMBERS HAS THE OPPOSITE ANSWER: assert the rendered text.** A value the app
formats itself — `total.ToString("C")`, `"N2"`, `"P0"` — renders in the browser **byte-identically to the server**,
so the string on screen is fixed by the app's own `app.DefaultCulture` and not by the machine the test runs on.
`Assert.Visible("£4,350.00")` is stable, and asserting only the underlying `Sum(…)` leaves the rendered money
untested — which is a real gap, because a page can compute the right number and draw it in the wrong place, or not
at all.

```osy title="assert what the page SHOWS, not only what the data holds" syntax
Assert.Visible("£4,350.00");     // en-GB `ToString("C")` — two decimals, and the grouping comma
Assert.Equal(4350m, Invoice.Sum(i => i.Amount));   // …the data too, if you want both
```

⚠ **Write the format's OWN spelling.** `ToString("C")` under `en-GB` is `£4,350.00`, not `£4,350` — the decimals are
part of the currency format. See [Culture formatting — ToString(format, culture)](https://osysharp.com/reference/stdlib/culture-formatting/) for which specifiers run in the browser (`N`/`F`/`C`/`P`
and the standard date ones) and which take a round trip.

⚠ **A `TimePicker` offers minutes in steps** (`minuteStep:`, 5 by default), so `09:37` is not a value the control can
hold. The refusal lists the minutes it does offer rather than failing somewhere deeper.

Use `Ui.Select` for a **dropdown** (above) and `Ui.Check`/`Ui.Uncheck` for a **checkbox or switch** — those hold a
choice rather than text, so filling them has nothing to write. A picker is the other way round: it holds a **value**,
not one of a list the app supplied, so `Ui.Select` on one is refused and names `Ui.Fill`. Each of these refusals
names the verb and the form that do work, so a wrong first guess costs one line rather than an investigation.

### Is the box ticked, and what has focus?        {#read-side}

```osy syntax
Ui.Check("Email me");
Assert.Checked("Email me", true);      // …and read it back
Assert.Focused("Notes");               // what the keyboard is on
```

**`Ui.Check` could only ever be written and `Ui.Press` acts on whatever has focus**, so an app could set both and
assert neither — which is exactly the gap that hides a control that stopped reflecting its own state, because every
test that only SETS one still passes.

⚑ **Focus is what makes keyboard behaviour testable at all**: focus order, focus-on-open, focus-return-when-a-dialog
closes. `Ui.Fill` focuses the field as typing does, so "fill it, press Enter" behaves the way a person does it.

⚠ **A control that does not SAY whether it is checked is REFUSED, not read as off.** Its state is a shape — legible
to a person and to nothing else — so answering "unchecked" would be a guess that passes forever. The fix is
`role: UiRole.Checkbox` + `checked:` (see [accessibility](https://osysharp.com/reference/ui/accessibility/)), which is the same line a screen reader needs.

### Is the menu open? Is the option selected?        {#states}

```osy syntax
Assert.Expanded("Project lead", true);      // the dropdown says it is open
Assert.Selected(ada, true);                 // …and this option says it is the chosen one
Assert.Probe("MarkdownEditor", "dirty", true);   // …and a foreign control's own internal facts
```

A menu's panel appearing and a chosen row's tint are **VISUAL** facts — legible to a person looking at them and to
nothing else. `expanded:` and `selected:` are the same facts said out loud, for a screen reader and for a test.

Without them the only available check was "some option's text appeared", which measures the app's **rendering**
rather than its **statement** — and passes just as happily on a control that announces itself closed forever. That is
a button that does something invisible.

**`Assert.Selected` names its option by VALUE**, exactly as `Ui.Select` does and for the same reason: a generic
control's options are drawn by its *caller's* template, so the text is a presentation choice that is ambiguous the
day two options read alike.

⚠ **"Does not say" is kept distinct from "says no" throughout.** A driver that answered `false` for a control with no
state at all would make `Assert.Expanded(x, false)` pass forever on the app whose menu never opens. Both refuse
instead, naming the one line of Osy# that fixes it — `role: UiRole.ComboBox` + `expanded:`, `role: UiRole.Option` + `selected:`
(see [accessibility](https://osysharp.com/reference/ui/accessibility/)).

**`Assert.Probe` is the same question asked of a FOREIGN control**, whose insides are not on the page to be read at
all. It reads the `probe { }` block the control's author published — see [probe — what a control says about itself](https://osysharp.com/reference/ui/control-probe/) for what to declare
and how the four ways it can fail are worded.

### Is this control actually refused?        {#authority}

```osy syntax
Assert.Enabled("Save");
Assert.Disabled("Delete organization");
Assert.DisabledBecause("Delete organization", "Only platform admins can delete organizations.");
```

`canPress:` reflects a declared policy onto a control — disabled when the policy does not hold — and `whenDenied:`
carries the app's own sentence. **These three are the only way to check that end to end**, and it is where a wrong
answer costs most: a control that SHOULD be refused and is NOT looks identical on screen to one that is, so an app
can ship the reflection missing entirely and nothing notices.

⚑ **Assert the SENTENCE, not just the refusal.** A control correctly refused with no explanation is a worse product
than one that says why, and the sentence is the app's own words — the thing the person actually gets.
`Assert.DisabledBecause` checks the refusal AND the reason together, deliberately: a reason on an operable control
would be a claim about nothing.

A control that is not on the screen is a failure naming what the page DOES show — not "disabled". "There is no
Delete button" and "it is enabled" are different findings, and only one of them is about authority.

⚠ **A match that is not a control is REFUSED.** A locator can land on a `div` that merely contains the words, and a
`div` has no operable state — so answering "enabled" for it would be a guess that passes forever. Name the control
itself, or render it as a `Pressable`/`Button`/`Link`.

⚠ **A checkbox that does not say whether it is checked is REFUSED, loudly.** `Ui.Check` puts a control INTO a state
rather than toggling it, so it has to read the current one. A control that reports no checked state cannot answer,
and clicking blind would tick an already-unticked box as readily as untick it — so the verb stops and says what is
missing.

The fix is the same one a screen reader needs, and it is one line of Osy#: give the control `role: UiRole.Checkbox` and
`checked:` (see [accessibility](https://osysharp.com/reference/ui/accessibility/)). The kit's own `Checkbox` and `Switch` carry them, so this only ever fires on a
control you wrote — which is the point. **A UI test is the first non-visual consumer your controls have ever had**,
so it finds exactly the gaps a screen reader would, before anyone using one does.

### A route that does not exist is a COMPILE error        {#routes}

```osy syntax
Ui.Visit("/ordrs");        // ⛔ no page declares this — named at compile time, with the routes that do exist
Assert.OnPage("/nope");    // ⛔ same check
Ui.Visit("/orders/42");    // ✓ satisfies [Page("/orders/{id}")]
```

**A test compiles together with the application it tests**, so the compiler holds both halves at once — the routes
the app declares and the routes the test asks for. Nothing else in the toolchain is in a position to compare them.

⚑ **The runtime failure this replaces is a bad one.** A typo'd route navigates fine, the router matches nothing, the
page renders nothing, and the test fails three lines later on an assertion about *content* — pointing at the
assertion, which is correct, instead of at the address, which is not.

A route built at runtime (`Ui.Visit(where)`) is not checked: there is nothing to check it against.

### How do I skip the login page?        {#signinas}

```osy title="one call gives the browser a genuine session" syntax
Ui.SignInAs(Mia);          // the browser is now Mia
Ui.Visit("/managers");     // …and her role decides whether she gets in
```

`Ui.SignInAs(P)` names a declared [`principal`](https://osysharp.com/reference/testing/runas/) and gives the BROWSER a real session for them. It is
the corollary of `runas`, not a variant of it: `runas` rebinds the principal on the ENGINE for a block, this one signs
the browser in.

⚑ **It bypasses the login PAGE, never authorization.** The ticket is genuine, so `[Authorize]`, a role gate,
`Candidates` and the entity's own read rules all still apply — the app decides what that person sees, exactly as in
production. That is the whole value, and it is also what stops the verb being a back door.

**Why it exists.** Driving the app's own signup form can only ever make you ONE person — the account it creates — so
"as a support manager" was unwritable, and every test carried three verbs of ceremony to become that one account.
Two people with different roles is the ordinary case for any app with roles, and nothing could express it.

```osy title="two people, one browser — the role decides what they see" syntax
Ui.SignInAs(Sam);
Ui.Visit("/managers");
Assert.Hidden("the managers' room");   // Sam holds no Manager grant — refused, by the app's own rule

Ui.SignInAs(Mia);                      // same browser, different person
Ui.Visit("/managers");
Assert.Visible("the managers' room");
```

It may be the FIRST thing a test does — before any `Ui.Visit` — in which case the session is carried into the first
screen. `Ui.SignOut()` ends it, and the next visit does not resurrect it.

⚠ **`Ui.SignInAs` is TEST-ONLY, and the compiler says so.** Minting a session for a user id with no credential is
impersonation anywhere else. In an app, sign somebody in with `Session.SignIn(ticket)` using a ticket an
`[AuthMethod]` returned.

⚠ **Signing in signs the WHOLE test in** — the same rule as a page-driven login (see
[#signed-in](#signed-in)): a row read beside the page is read as that person too, so the screen and the database
never disagree about who is looking.

### How do I assert EXACT text, not "contains"?        {#exact-text}

`Assert.Visible` is CONTAINS: it means "these characters appear somewhere, in something". That is right for prose
and wrong for a VALUE — it cannot tell a cell reading `0` from one reading `10`, nor a status of "on time" from one
reading "not on time".

```osy syntax
Assert.Visible("0");            // ✓ …and also passes on a page whose only number is 10
Assert.TextIs("0");             // something reads exactly "0"
Assert.TextIs("0", within: card);   // …in THIS card, which is nearly always what you meant
```

Whitespace is collapsed on both sides, so a phrase broken across lines by the layout still matches. `within:`
applies, and an exact read usually wants it: page-wide, `Assert.TextIs` asks whether ANY element reads that, which
is a much weaker claim than "this row says 0".

⚑ **The failure tells you WHICH kind it is, and that is the point.** Two very different things look identical
through a CONTAINS lookup:

| what happened | what `Assert.TextIs` says | where the fix is |
|---|---|---|
| nothing says it | `nothing reads exactly '…'`, with what the page does show | the data, the filter, the route |
| the words are there, in SEPARATE elements | `… is on the screen, but SPLIT ACROSS SEVERAL ELEMENTS` | the markup, or assert one part |

The second is worth its own sentence because it is invisible in the source you are reading. `Row { Text("0");
Text("breached"); }` renders as one phrase to a person and as two elements to everything else — so
`Assert.Visible("0 breached")` matches nothing while both words sit plainly on screen, and the stray `breached`
breaks an `Assert.Hidden("breached")` somewhere else at the same time. One markup detail, two misleading failures.

### Naming what to click — never rename the page for a test        {#strict}

> **Read this while you are WRITING the page, not after a locator has refused.** Most of the ambiguity people brace
> for does not exist: an exact match beats a longer label that merely contains it, so the button, the card and the
> heading all keep the words a person should read. The rest of this section is what to do on the day two things
> genuinely collide — and it is never a rename.

**A locator that matches two different things REFUSES.** It does not pick one.

```osy title="two matches refuse — the two things `within:` takes" syntax
Ui.Click("Edit");                      // ⛔ every row has an Edit — refused, naming what it found
Ui.Click("Edit", within: order);       // ✓ that order's row
Ui.Click("Save", within: "Billing");   // ✓ the container with that `label:`
Assert.Enabled("Delete", within: row);
```

⚑ **Why refusing beats guessing.** Every ancestor of a match also contains its text, so a locator has to prefer the
innermost — and once it is choosing by depth, two genuinely different matches are decided by *how deeply the app
nests them*. That is right by luck and silently wrong the day somebody adds a wrapper. An exact match still wins over
a substring, so adding a "Save changes" button does not make an existing `Click("Save")` ambiguous.

⚑ **THE CASE PEOPLE PRE-EMPTIVELY REWRITE THEIR PAGE OVER, WRITTEN OUT AND COMPILED.** A `Button("Add")` inside a
`Card("Add an expense")` is the one that looks alarming: the card title contains the word the test presses. It
resolves, because the button is an EXACT match and the title is only a containing one — so the button stays "Add",
which is what a person should read on it, and the test stays `Ui.Click("Add")`:

```osy title="a Button `Add` inside a Card `Add an expense` — the exact match wins" test app=testing-ui-strict
using Osysharp.Ui;

entity Expense {
  [Required, MaxLength(200)] string Description;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

[Page("/expenses")]
[AllowAnonymous]
[Render(CSR)]
component ExpensesPage() {
  string draft = "";

  void Add() {
    new Expense { Description = draft };
    draft = "";
  }

  render {
    Stack {
      Card("Add an expense") {                 // the TITLE contains the word "Add"…
        Field("Description", value: draft);
        Button("Add", onPress: Add);           // …and the BUTTON is still just "Add"
      }
      Card("Expenses") {
        foreach (var e in Expense.OrderBy(x => x.Description)) { Text(e.Description); }
      }
    }
  }
}

[Test]
void the_button_keeps_the_name_a_person_should_read() {
  Ui.Visit("/expenses");
  Assert.Visible("Add an expense");   // the containing title really is on the screen

  Ui.Fill("Description", "Taxi");
  Ui.Click("Add");                    // ✓ the exact match wins — no rename, no `within:` needed

  Assert.Visible("Taxi");
}
```

So do not pre-emptively rename that button to `Add expense`, and do not redesign the card to get the word out of its
title. Neither buys anything the platform has not already given you, and both cost the page a word a person was
meant to read. **If you are unsure, write the page the way it should READ and let the test tell you** — a genuine
collision refuses loudly, by name, and the fix is one `within:`. Nothing about it is silent, so there is nothing to
insure against in advance.

**`within:` names a container by its `label:`, or a ROW BY WHAT IT IS** — an entity, an enum member, a string — the
same way `Ui.Select` and `Assert.Before` name theirs. So `within: order` finds that order's row without the test
knowing what the row happens to render, and it keeps working when the template changes.

⚑ **A TITLED CONTROL ALREADY HAS THAT `label:` — you do not add one.** `Card("Needs watering")` passes its title
down as the surface's `label:`, so the card is addressable as `within: "Needs watering"` with nothing extra written.
That is the usual way a page gets its scopes: give the two lists that both render a `Water` button a `Card` title
each, and every ambiguous locator in the file resolves.

```osy title="two lists, one button label — a Card title IS the scope" syntax
Card("Needs watering") { foreach (var p in due)  { Row { Text(p.Name); Button("Water", onPress: () => Water(p)); } } }
Card("Your plants")    { foreach (var p in all)  { Row { Text(p.Name); Button("Water", onPress: () => Water(p)); } } }

Ui.Click("Water", within: "Needs watering");   // ✓ the due list's button, not the other one
```

⛔ **DO NOT REWORD THE PAGE TO MAKE A TEST PASS.** A locator that refuses, or an `Assert.Hidden` that matches text
you did not mean, is asking you to say WHERE — not asking the app to be called something else. Renaming a card,
a button or a heading to dodge a match changes what a person reads to satisfy a test, and the collision comes back
the next time two things legitimately share a word.

⚑ **AND BEFORE YOU CHANGE ANYTHING, LOOK AT THE PAGE — `Ui.Screen()`** ([[testing-ui#screen]]). A refused or missed
locator is exactly the moment the screen is worth printing: it asserts nothing, never fails, and shows you the labels
and the containers that are actually there. Almost every rewrite in this section is a guess about what the page
renders, made by someone who could have read it in one line.

⚑ **A STRING scope matches a row that CONTAINS it** — the same containment `Assert.Visible` uses, not an exact match
on the row's whole text. `within: "Mistborn"` finds the row that mentions Mistborn; the row also renders an author, a
badge and three buttons, and none of that has to be spelled out. (Ambiguity still refuses: two rows both mentioning
it is a refusal, not a guess.)

⚠ **AND WHEN ONE ITEM IS LISTED TWICE, NAMING IT HARDER CANNOT HELP** — a page showing the same entity in two lists
(a roster and an editor) gives a row scope two matches of the SAME row, because a row is matched by its IDENTITY
before its text. There is nothing unique left to name. Say which LIST instead, and let the row resolve inside it:

```osy title="the SAME row listed twice — scope to the list, not the row" syntax
Ui.Within("Scores") {                                  // the list, by its `label:`
  Assert.Hidden("Grace Hopper", within: "Ada Lovelace");  // …then Ada's row, inside it
}
```

This is why a container worth scoping to is worth giving a `label:`.

### How do I click an icon button with no text?        {#accessible-name}

**Every locator matches the ACCESSIBLE NAME, and falls back to visible text — in that order.** So an `IconButton`
that renders a glyph is found by the `label:` it declares, not by the glyph:

```osy syntax
IconButton(onPress: Remove, label: "Delete") { Icon(Icons.Trash); }   // renders an icon…
Ui.Click("Delete");                                             // …and is pressed by its label
```

This is why `label:` is worth setting on anything without words in it: it is the same string a screen reader
announces and the same string a test presses, so an unnamed icon button is unreachable to both.

### Scoping several calls at once — `Ui.Within`        {#within-block}

> **Inside a dialog you do not need this at all** — a modal already scopes every locator to itself. See
> [[testing-ui#dialogs]]. Reaching for `Ui.Within("<the dialog's title>")` there is the common wrong turn: the
> dialog IS the container with that name, so there is no inner one to find and the call is refused.

**To name a ROW, pass the row — not a word it renders.** A test that already holds the entity holds the only
unambiguous handle there is, and it needs nothing on screen:

```osy title="one row of many — pass the row, never a word it renders" syntax
var fern = Plant.Single(p => p.Name == "Fern");

Ui.Click("Water (due)", within: fern);          // that row's button
Assert.Hidden("Water (due)", within: cactus);   // another row saying it is not this row's business
```

⛔ **Never change what the app RENDERS to make a locator unambiguous.** Renaming a `Water (due)` button so
`Assert.Hidden("Water")` passes, or collapsing two lists into one so a substring stops matching twice, makes the TEST
a reason to keep a design choice — and the scoped form above already answers it. Scoping by the row is matched on
IDENTITY, so it keeps working when the row's text changes, and another row reading alike cannot steal it.

⚠ **If the SAME row is rendered TWICE — a "Due now" card and an "All plants" card below it — its identity is on
screen twice, and naming the row alone is ambiguous.** That is not a reason to go back to text. Name the container
first and the row inside it; scopes COMPOSE, outer to inner:

```osy title="the SAME row in two cards — name the card, THEN the row: scopes COMPOSE" syntax
Ui.Within("Due now") {
  Ui.Click("Water", within: fern);      // that card's copy of that row, and nothing else
}
```

⚑ This is the case a two-card page always has, so reach for it before reaching for text. Naming only the card works
until a second row is due.

When more than one call belongs to the same container, name it once:

```osy title="several calls, one container — name it once" syntax
Ui.Within("Edit book") {
  Ui.Fill("Title",  "Mistborn");
  Ui.Fill("Author", "Brandon Sanderson");
  Ui.Click("Save changes");
}
```

This is **sugar for the `within:` argument above** — it adds one to every locator in the block and changes nothing
else, so there is no second scoping mechanism to learn. Three rules, and each is the obvious one:

- **A `within:` written at the call site is resolved INSIDE the block's container.** The nearer scope still decides
  what a locator sees — it is just looked for within the one you already named, rather than starting again from the
  whole page. `Ui.Within("Scores") { Ui.Click("Edit", within: "Ada Lovelace"); }` means *Ada's row, in the Scores
  list*.
- **A verb that locates nothing is left alone** — `Ui.Visit`, `Ui.Press`, `Ui.Viewport`, `Ui.Back`/`Forward`,
  `Ui.SignInAs`/`SignOut`. A navigation inside the block is still just a navigation.
- **It nests**, by the same rule: an inner block narrows inside the outer one, so the scopes COMPOSE into a path
  rather than replacing one another. Three nested blocks are three segments, outer first.

It takes whatever `within:` takes — a container's `label:`, or a row named by what it is.

A scope that names nothing fails **at the scope**, not later as a missing button — "there is no Edit here" would
send you to look at the wrong thing entirely.

**`within:` narrows what a test can SEE, not only what it can click.** `Assert.Visible`, `Assert.Hidden` and
`Assert.TextIs` all read inside the scope:

```osy title="`within:` narrows what a test can SEE, not only what it can click" syntax
Assert.Hidden("on time", within: breachedCard);   // this card must not say it — other cards may
```

⚑ **`Assert.Hidden` is the one that needs this most**, and the one that is useless without it. A scoped `Visible`
usually passes either way, because the text is normally inside the row you named as well as on the page. `Hidden`
inverts that: page-wide it goes red exactly when some OTHER row says the word, which on a list is the normal state
of the world.

### While a dialog is open        {#dialogs}

**A modal makes the screen behind it inert, and the verbs follow that.** While a dialog is open, every locator —
`Ui.Click`, `Ui.Fill`, `Ui.Select`, `Assert.Enabled` — reaches only INSIDE the dialog. This needs no extra
ceremony and there is no "click in the dialog" verb: it is simply what a modal means.

```osy syntax
Ui.Click("Delete");                    // the page's button — opens the confirm
Assert.Dialog("Delete this order?");   // it opened, and it is the RIGHT one
Ui.Click("Delete");                    // the CONFIRM's button; the page's is behind a scrim
Assert.Visible("Deleted");
```

⚑ **This is why the two lines above are not ambiguous.** A confirm dialog repeats the word on the button that
opened it — "Delete" → *Delete this order?* → "Delete" — so a page-wide locator has two equally good matches and
must break the tie somehow. Breaking it by position in the page is how a test comes to press a control the user
could not have pressed, leave the dialog open, and still go green.

A dialog opened from a dialog scopes to the **innermost** one, for the same reason: that is the only one in front
of the person. Answering it gives the previous scope back.

**`Assert.Dialog(title)` is how you ask whether a modal is up, and which.** `Assert.Visible(title)` cannot answer
it: it means "somewhere on screen", so it passes on a page that merely MENTIONS those words and on one whose dialog
never opened. It stays page-wide deliberately — narrowing it would make a failing test hide the page you need to
see — so the two verbs answer different questions and both are worth having.

⚠ **A dialog that is open and names itself to nobody is REFUSED, not failed.** It is on screen and working; it
simply cannot be identified by its title, so reporting "wrong title" would send you to fix a title that is correct.
The refusal names the fix, and it is one line — give the panel `role: UiRole.Dialog` and `label: <its title>` (see
[accessibility](https://osysharp.com/reference/ui/accessibility/)). The kit's own `Dialog` already carries both, so this only ever fires on a panel you drew
yourself. **A UI test is the first non-visual consumer your dialogs have ever had.**

## Verbs that are deliberately absent        {#preview}
Everything above this line exists. What follows is the short list of things a reader reasonably expects to find here
and will not — each with what to reach for instead.

⚠ **There is no `Ui.ClickHeader`, because `Ui.Click` already is one.** A column header is something a person
clicks, so it is clicked by the same verb as everything else — `Ui.Click("Total", within: "Reports")` re-sorts the
grid under that header, and [#order](#order) is the assertion that proves the rows actually moved. A dedicated verb
would name a second way to do one thing.

⚠ **This section was headed "Not built yet", and that heading cost more than the gap it described.** It kept
listing verbs as unbuilt long after they had shipped, and a single unbuilt fence made `osy docs testing-ui` open
with a page-wide warning that "a surface does not exist yet" — the first line a reader who came here to learn UI
testing ever saw. `Assert.Enabled` / `Assert.Disabled` are real, documented under [#authority](#authority). So is
`Assert.Dialog`, under [#dialogs](#dialogs), and the cell assertion, which shipped as
`Assert.Cell(row, column, expected)` under [#cell](#cell) — a row is addressed by what it IS, so it needs no grid
operand. `Assert.RowCount` is [`Assert.Items`](#items).

## What this page CANNOT ask — geometry        {#geometry}

⛔ **Every assertion on this page is about TEXT or STATE, and all of them pass on a screen that is visually broken.**
`Assert.Visible("Confirm")` holds for a button with a banner drawn over it; `Assert.Enabled("Save")` holds for a
button laid out past the edge of its own card; `Assert.Visible("Quarterly revenue report")` holds for a chip
rendering `Quarterly rev…`, because the DOM carries the whole string whether the box shows it or not.

That is not a gap in the locators — it is what the renderer can see. `osy test` renders in happy-dom, which has no
font engine and no compositor, and it is fast enough to run on every change precisely because of that.

The geometric claims live next door in **[Layout assertions — is it actually usable on screen?](https://osysharp.com/reference/testing/ui-layout/)** — `Assert.Clickable`, `Assert.FitsOn`,
`Assert.Above`, `Assert.Inside` and the rest — and are checked by `osy test --pixels`, which drives a real browser.
Under plain `osy test` they report themselves NOT CHECKED, never green. `Ui.Shot("label")` photographs the page
beside them.

## See also       {#see-also}
- [Layout assertions — is it actually usable on screen?](https://osysharp.com/reference/testing/ui-layout/) — the geometric claims this page cannot make, and `osy test --pixels`
- [pointer](https://osysharp.com/reference/ui/pointer/) — `onPointerEnter`/`onPointerLeave`, the half of hover an app can ACT on; `Ui.Hover` under
  `--pixels` is what proves the action runs
- [Assert](https://osysharp.com/reference/testing/assert/) — the rest of the assertions, which work here unchanged
- [runas](https://osysharp.com/reference/testing/runas/) — running a test as a principal, which is what makes the screen theirs
- [[Test] / [TestFixture]](https://osysharp.com/reference/testing/test/) — `[Test]` and `[TestFixture]`
- [runas](https://osysharp.com/reference/testing/runas/) — the other way to be a principal, and why it does not apply here


---

<!-- https://osysharp.com/reference/testing/redeem-callback/ -->

# Workflow.Redeem (answer a callback URL, as nobody)

> Answers a link minted by `<Slot>.CallbackUrl()` the way the third party holding it would — anonymously, with no principal of any kind. It is the only way to test the half of a callback URL that is the point of one: somebody with no account completing a slot. Refusals arrive as ordinary faults, so `Assert.Throws` reads them.

<!-- id: testing-redeem-callback · area: testing · stability: preview · html: https://osysharp.com/reference/testing/redeem-callback/ -->

## Summary        {#summary}
[`<Slot>.CallbackUrl()`](https://osysharp.com/reference/workflow/callback-url/) exists so that a person with **no account** can complete one slot —
a supplier confirming a delivery, an invitee accepting. Every other driver verb in a `.test.osy` acts *as somebody*,
so without this one an app could assert that a link was **minted** and could never assert that answering it works.

`Workflow.Redeem(url)` answers it. It runs **genuinely anonymous** — it does not pass the test's `[runas]` principal,
or any principal — so what a passing test proves is that no principal was involved.

**The argument is the callback URL, or just its last path segment — the token.** Both are accepted, so a page that
mails `App.BaseUrl + "/accept/" + token` and a test that passes the whole `AcceptLink` are answering the same slot
([signup by invitation (invite, accept link, chase, expire)](https://osysharp.com/reference/security/invitation-signup/) passes the token; the examples below pass the link). Nothing else is parsed out of
the string.

## Signature      {#signature}
```osy syntax
Workflow.Redeem(<url-or-token>);        // an event with no parameters — the whole callback URL, or its last segment
Workflow.Redeem(<url-or-token>, <json>); // the event's arguments, as the JSON a third party would POST
```

`<url>` is the string `CallbackUrl()` returned. `<json>` is text, not a typed argument list — deliberately: the wire
contract is *"POST the event's parameters as a JSON object, by name"*, so the test drives the same bytes a stranger's
`curl` would. A typed form would prove something weaker by skipping the bind that the contract is made of.

## Description    {#description}

### Refusals are faults, so `Assert.Throws` reads them   {#refusals}
A refusal is not a return value to inspect — it arrives the way every other refusal in this surface does:

| what happened | the fault |
|---|---|
| the token is unknown, or was already spent | `NotFoundException` |
| the slot has closed — satisfied, cancelled, or its run finished | `ConflictException` |
| the body does not fit the event's signature | `ValidationException` |
| a `Requires` criterion does not hold | `RequirementsNotMet` |
| a gate the callback still meets refused (a `Pending` slot) | `NotAuthorized` |

⚑ **Unknown and SPENT are the same answer on purpose.** Telling an unauthenticated caller which of the two it hit
tells it that a token exists. A CLOSED slot is distinguishable because the holder needs to know their item was
withdrawn rather than that their link was corrupted.

### What it does around the call   {#settling}
It settles staged writes first — the URL almost always came off a row the test just created, and the run has to exist
before a token can address it — and after a successful deposit it drives the background pump and **drops what the
test's context had already loaded**. The deposit happens on the engine's own context, so without that last step the
next line reads pre-deposit values and a callback that really worked reads as one that silently did nothing.

## Examples       {#examples}
A supplier who is not a user of the app, and cannot become one — `Candidates` admits only staff, so the link is the
only way this run can reach `Confirmed`:

```osy title="the model" test app=testing-redeem-callback
enum OrderState { Awaiting, Confirmed, Refused }

[Principal]
entity Person {
  [Required, MaxLength(100)] string Name;
  bool IsStaff;
  security { allow read, create when IsAuthenticated || IsAnonymous; }
}

entity Order {
  [Required, MaxLength(100)] string Reference;
  OrderState State;      // no default: the workflow autostarts, so `Initial = Awaiting` IS this field's value
  [MaxLength(400)] string? ConfirmLink;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow OrderFlow {
  Tracks = Order.State; Autostart = true; Initial = Awaiting;

  event Confirm(bool ok);

  state Awaiting {
    subscribe Confirm(bool ok) as SupplierOk { Candidates = u => u.IsStaff; }
    enter { this.Item.ConfirmLink = SupplierOk.CallbackUrl(); }
    on SupplierOk(bool ok) {
      when (ok) { goto Confirmed; }
      default   { goto Refused; }
    }
  }

  terminal success Confirmed { }
  terminal cancel  Refused { }
}
```

```osy title="…and the tests only this verb makes possible" run app=testing-redeem-callback
[TestFixture]
void Seed() {
  new Person { Name = "Sam", IsStaff = true };
}

principal Sam => Person.Single(p => p.Name == "Sam");

// THE FEATURE: a caller with no account completes the slot, while the same deposit made as a signed-in
// NON-candidate is refused. Both halves in one test — a gate that admitted everyone would pass either alone.
[Test(Seed)]
[runas(Sam)]
void a_holder_of_the_link_confirms_with_no_account_at_all() {
  var o = new Order { Reference = "PO-1" };
  Workflow.Settle(o);
  Assert.NotNull(o.ConfirmLink);

  Workflow.Redeem(o.ConfirmLink, "{\"ok\": true}");

  Assert.Equal(OrderState.Confirmed, o.State);
}

// SINGLE-USE — a forwarded link cannot be answered by a second party, which is a different problem from a
// double click and the one that actually bites.
[Test(Seed)]
[runas(Sam)]
void a_forwarded_link_is_dead_once_it_has_been_used() {
  var o = new Order { Reference = "PO-2" };
  Workflow.Settle(o);
  var link = o.ConfirmLink;

  Workflow.Redeem(link, "{\"ok\": true}");
  Assert.Equal(OrderState.Confirmed, o.State);

  Assert.Throws<NotFoundException>(() => Workflow.Redeem(link, "{\"ok\": true}"));
}

// THE PAYLOAD IS CHECKED, NOT TRUSTED — it is bound against the event's own signature, so a property the event
// never declared is refused rather than ignored.
[Test(Seed)]
[runas(Sam)]
void a_body_the_event_does_not_declare_is_refused() {
  var o = new Order { Reference = "PO-3" };
  Workflow.Settle(o);

  Assert.Throws<ValidationException>(() => Workflow.Redeem(o.ConfirmLink, "{\"approved\": true}"));
  Assert.Equal(OrderState.Awaiting, o.State);
}
```

⚠ **Note the `[runas(Sam)]` and the fact that it changes nothing about the redemption.** The attribute governs the
rest of the body — creating the order is Sam's act. Answering the link is nobody's.

And the property most worth pinning, because a reader will not guess it — an event's `[Authorize]` is not evaluated
either, since a predicate takes a principal and a callback has none:

```osy title="an Authorize refuses a stranger — the link is still accepted" syntax
// The event declares [Authorize(u => u.Email == this.Item.Email)] — and a signed-in stranger IS refused by it…
runas (Mallory) { Assert.Throws<NotAuthorized>(() => Onboarding.RaiseAccept(inv)); }
// …while the LINK, held by nobody, is accepted.
Workflow.Redeem(inv.AcceptLink);
```

## See also       {#see-also}
- [Callback URLs — letting an outsider complete one slot](https://osysharp.com/reference/workflow/callback-url/) — minting the link, and the five properties that bound it
- [signup by invitation (invite, accept link, chase, expire)](https://osysharp.com/reference/security/invitation-signup/) — the complete flow these examples are drawn from
- [runas](https://osysharp.com/reference/testing/runas/) — acting AS somebody, which this verb is the absence of


---

<!-- https://osysharp.com/reference/testing/test/ -->

# [Test] / [TestFixture]

> A test is an ordinary function marked [Test]. It runs against a throwaway clone of the app, so it may create rows and break rules freely. A [TestFixture] seeds the data once and every test forks from it.

<!-- id: testing-test · area: testing · stability: stable · html: https://osysharp.com/reference/testing/test/ -->

## Summary        {#summary}
A test is an ordinary function marked `[Test]`. It runs against a **throwaway clone** of the app — its own database,
made for it and thrown away after — so it can create rows, violate rules and assert on the wreckage without touching
anything real, and without cleaning up after itself.

A `[TestFixture]` builds the starting data **once**; every test that names it forks from that state.

## Signature      {#signature}
```osy syntax
[TestFixture]
void <Seed>() { … }               // build the starting data, once

[Test]
void <Name>() { … }               // a test with no fixture

[Test(<Seed>)]
void <Name>() { … }               // a test that starts from <Seed>'s data
```

## Description    {#description}

### A test is a function   {#a-function}
There is no separate test language. A test is a function: it can call your functions, create rows, run queries — the
whole model is in scope.

```osy title="a model" test app=testing-test
entity Order {
  [Required] string Code;
  decimal Total;

  security { allow create, read when IsAuthenticated || IsAnonymous; }   // an entity with no security block is denied to everyone
}

void PlaceOrder(string code, decimal total) {
  var o = new Order { Code = code, Total = total };
}
```

```osy title="…and a test of it" run app=testing-test
[Test]
void Placing_an_order_stores_its_total() {
  PlaceOrder("A1", 42m);
  Assert.Equal(42m, Order.Single(o => o.Code == "A1").Total);
}
```

#### A row a function hands back is a live row of the test's unit of work   {#returned-rows}
A function runs in a child scope of its caller's unit of work, committed upward when it returns. The row it returns
— created there, or merely loaded there — is **the same live row** the test would have got from a query: assign to
it and the write is staged in the test's unit of work, and the next `Assert.*` flushes it like any other. Nothing to
re-read, nothing to re-attach.

```osy title="a function that returns what it made" test app=testing-test-returned
entity Organization {
  [Required, MaxLength(100)] string Name;
  [Required, Unique, MaxLength(60)] string Slug;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

Organization Found(string name, string slug) {
  var org = new Organization { Name = name, Slug = slug };
  UnitOfWork.Commit();      // with or without this — a merely staged return value is tracked the same way
  return org;
}
```

```osy title="…and the row it returned, written to" run app=testing-test-returned
[Test]
void A_returned_row_is_still_the_tests_row() {
  var acme = Found("Acme", "acme");
  acme.Name = "Acme Travel";                                          // a staged write in THIS test's unit of work
  Assert.Equal("Acme Travel", Organization.Single(o => o.Slug == "acme").Name);   // read back through a fresh query
}
```

The one row that is **not** live is one created in a scope that then faulted — a `try` body or a call that threw
after making it. That row never existed, and reading or writing it is refused by name: *the Organization row … was
created in a scope that has since been discarded*. Create it where it is wanted, or catch the fault inside the scope
that created it.

**The same is true after an `Assert.Throws` or `Assert.Denied` catches a commit fault.** The refusal that assert was
written to prove is a failed transaction, so everything the test had staged and not committed is thrown away with
it — otherwise the same violation trips again at the next assert and errors the test *after* it has already passed.
A row the test **created** before that point is therefore gone too, and touching it afterwards says so: *the
Organization row … was created and then DISCARDED*. Seed through a [[#fixture|`[TestFixture]`]], or commit what you
want to keep, before the assert that expects a fault.

### A fixture seeds once; tests fork from it   {#fixture}
Building the same three customers at the top of nine tests is slow and, worse, it is nine places to change. A
`[TestFixture]` builds them once:

```osy title="a fixture, and two tests that fork from it" run app=testing-test
[TestFixture]
void Seeded() {
  PlaceOrder("A1", 100m);
  PlaceOrder("A2", 50m);
}

[Test(Seeded)]
void The_seeded_orders_are_there() {
  Assert.Equal(2, Order.Count());
}

[Test(Seeded)]
void A_new_order_is_not_seen_by_its_siblings() {
  PlaceOrder("A3", 5m);
  Assert.Equal(3, Order.Count());     // this test's own clone: the seeded two, plus this one
}
```

The second test creates a third order and sees three. The first test still sees two. **Tests never see each other's
writes** — each forks its own copy of the fixture's data, so they can run in any order, or at the same time, and
neither has to undo anything.

That isolation is the whole point: a test suite where one test's leftovers change another's outcome is a suite that
fails at random and gets ignored.

### The fixture is unsecured; the test body is not   {#security}
They do not run under the same rules, and this catches everyone once:

- a **`[TestFixture]`** seeds **unsecured** — it can create rows across every entity, including ones nobody is allowed
  to create, so building a scenario never means weakening a rule;
- a **`[Test]` body** runs **secured, as an initially-anonymous principal** — your rules are on, and nobody is signed
  in.

So a test that simply calls a function may be **denied**, and that is the system working. To act as a real user, name
a principal and run as them ([[runas(Name)] test attribute and principal selectors](https://osysharp.com/reference/testing/runas-attribute/)). The whole model is in [the testing guide](https://osysharp.com/reference/testing/index/).

### What should a test be called?   {#naming}
A test's name is read by a person deciding whether the failure matters. `A_new_order_is_not_seen_by_its_siblings`
tells them; `Test3` does not.

### Parking a test with `[Skip]`   {#skip}
A test that cannot run yet — it depends on a capability the platform doesn't offer, or an intended behaviour that
isn't built — is not deleted. Marking the `[Test]` with **`[Skip("reason")]`** keeps it in the suite as the executable
record of the intended behaviour: it is still **discovered** and listed, but never **run**, and every surface renders
it as a skip. The reason string is **required** — it is the visible note of *why* the test is parked:

```osy title="a parked test" test app=testing-test
[Test]
[Skip("KNOWN-GAP: refunds aren't implemented yet")]
void A_refund_restores_stock() {
  // The intended behaviour, written out — it compiles, so it can't rot, and it turns on
  // the day the gap closes and the [Skip] comes off.
  Assert.Equal(0, Order.Count());
}
```

`[Skip]` applies only to a `[Test]` (a `[TestFixture]` cannot be skipped), and its body must still **compile** — the
whole value of a parked test is that it is a real, type-checked spec, not a comment.

## See also       {#see-also}
- [Testing (real app, real data, real rules)](https://osysharp.com/reference/testing/index/) — the testing guide: isolation, who a test acts as, and how you prove a rule
- [Assert](https://osysharp.com/reference/testing/assert/) — the assertions, including `Assert.Throws` and `Assert.Denied`
- [[runas(Name)] test attribute and principal selectors](https://osysharp.com/reference/testing/runas-attribute/) — `principal` declarations and `[runas(Name)]`
- [runas](https://osysharp.com/reference/testing/runas/) — running a test as a particular user, to test security
- [Running tests locally](https://osysharp.com/reference/testing/running-tests-locally/) — `osy test`


---

<!-- https://osysharp.com/reference/testing/runas-attribute/ -->

# [runas(Name)] test attribute and principal selectors

> A test runs deny-all as an anonymous principal, so to read or write real data it must act AS a seeded `[Principal]`. Declare a named selector with `principal Name => <query>;`, then put `[runas(Name)]` on a `[Test]` to run its whole body as that principal. The `runas(){}` block form stays for the rarer case of switching principals mid-test. `[runas]` binds to a fixture-seeded row and never creates one.

<!-- id: testing-runas-attribute · area: testing · stability: preview · html: https://osysharp.com/reference/testing/runas-attribute/ -->

## Summary        {#summary}
An app is **deny-all by default**, and a `[Test]` runs in a secured context as an **initially-anonymous** principal.
So any test that reads or writes real data must act **as** some seeded `[Principal]` row. Two pieces make that
declarative:

- **`principal Name => <selector>;`** — a top-level declaration that names a selector resolving, at runtime, to a
  single `[Principal]` row (e.g. `principal Alice => User.Single(u => u.Name == "Alice");`). It mirrors a
  `policy Name => …;`, but the body is a single-entity query, not a boolean.
- **`[runas(Name)]`** — an attribute on a `[Test]` that runs the **whole body** as that principal.

The `runas(<expr>) { … }` **block** form is still there for the case it is uniquely good at: **two principals in one
test** (Alice creates a row; then as Bob, assert he cannot see it). Attribute = whole test; block = a sub-region;
the inner block wins for its span.

## Signature      {#signature}
```osy syntax
principal <Name> => <single-entity selector>;   // names a seeded [Principal] row

[Test(<Fixture>)]
[runas(<Name>)]                                  // the whole body runs as <Name>
void <TestName>() { … }
```

## Description    {#description}
`[runas(Name)]` **binds, never creates.** The named selector must resolve to a row the fixture already seeded; a
selector that matches nothing fails the test loudly (it is `User.Single(…)`, not a silent fallback). Naming a
principal that was never declared is a compile error, as is putting `[runas]` on a function that is not a `[Test]`.
This is deliberate — if `[runas]` could conjure a principal, a denial test would be fake, quietly passing against a
principal that production would never grant.

Running as a principal rebinds **more than the row**: the effective **role** list is resolved for that principal
(so a role-gated `allow read when …` activates only for a principal actually granted the role), and role-dependent
row filters re-evaluate against the acting user. Data staged during the test **settles as that principal** at the end
of the run, so an owner-scoped write commits under the owner with no extra ceremony.

A declared **principal name is also usable as a value**: writing `Alice` where a value is expected resolves to that
same seeded row (its selector), so `new Doc { Owner = Alice }` reads naturally — no `var alice = User.Single(…)`
re-query. An ordinary local, parameter, or entity of the same name always shadows it (the principal name is a
last-resort resolution, never an override).

Use the attribute for the common case — "this whole test runs as one principal" — and reach for the `runas(){}`
block only when a single test genuinely needs to change principals partway through, or must assert **outside** the
run-as region (for example, to observe what committed after the block settled).

A `principal` declaration is authored in a test context alongside the `[TestFixture]` that seeds its row; the fixture
seeds unsecured (so it can create freely), and the selector gives that row a compile-time name the attribute carries.

## Examples       {#examples}

A single-principal read test — the whole body runs as Alice, who sees only her own owner-scoped rows:

```osy title="the model — locked, the way a real app is" test app=runas-example
[Principal]
entity User {
  [Required, MaxLength(60)] string Name;
  security { allow read where Id == user.Id; }   // you can read yourself. Nobody enumerates the user table.
}

entity Doc {
  [Required] User Owner;
  [MaxLength(200)] string Title;
  security {
    allow create when IsAuthenticated;
    allow read where Owner == user;
  }
}
```

```osy title="whole-body run-as" run app=runas-example
principal Alice => User.Single(u => u.Name == "Alice");
principal Bob   => User.Single(u => u.Name == "Bob");

[TestFixture]
void Seed() {
  var alice = new User { Name = "Alice" };
  var bob = new User { Name = "Bob" };
  var d = new Doc { Owner = alice, Title = "alice-doc" };
}

[Test(Seed)]
[runas(Alice)]
void Alice_sees_only_her_own_rows() {
  Assert.NotNull(Doc.FirstOrDefault(d => d.Title == "alice-doc"));
}

// Two principals in one test — the case the attribute alone cannot express. `[runas(Alice)]` is the body's
// default; the inner `runas(Bob) { }` block wins for its span.
[Test(Seed)]
[runas(Alice)]
void Alice_creates_Bob_cannot_see() {
  var secret = new Doc { Owner = Alice, Title = "secret" };   // `Alice` as a value — the seeded row
  runas(Bob) {
    Assert.Null(Doc.FirstOrDefault(d => d.Title == "secret"));
  }
}
```

Note that the `User` table above is **locked** — nothing may enumerate it, which is how you would really write it. The
selector still finds Alice, because binding *who a test acts as* is scaffolding, not an app data read: it resolves the
same way the fixture seeds, past the app's own rules. **You never have to loosen the user table to test your
security.**

The second test is a real proof rather than an illustration: the row is created by one user and genuinely **not
selected** for the other. The rule is inside the query, so there is nothing for Bob to be "hidden" from.

## See also       {#see-also}
- [Testing (real app, real data, real rules)](https://osysharp.com/reference/testing/index/) — the testing guide: who a test acts as, and how you prove a rule
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — why a test starts deny-all and anonymous
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — the `[Principal]` marker and who a request is
- [Running tests](https://osysharp.com/reference/testing/running-tests/) — authoring and running `[Test]` / `[TestFixture]`


---

<!-- https://osysharp.com/reference/testing/runas/ -->

# runas

> Runs a block as a given principal, so security rules apply exactly as they would for that user. It is how you test that a rule denies the people it should — the only way to prove security from inside the app. It is TEST-ONLY: a `runas` outside a `[Test]` is a compile error, because app code may not step outside the security it declared.

<!-- id: testing-runas · area: testing · stability: stable · html: https://osysharp.com/reference/testing/runas/ -->

## Summary        {#summary}
`runas (principal) { … }` runs the block **as that user**. Every security rule inside it evaluates against them: row
filters bind to them, role rules activate for their roles, and what they may not do is refused.

It exists so you can test the thing that is hardest to test and worst to get wrong — that your rules deny the people
they should.

**It works only inside a `[Test]`.** Writing `runas` anywhere else is a compile error, and that is deliberate — see
[Why it is test-only](#test-only) below.

## Signature      {#signature}
```osy syntax
runas (<principalRow>) {
  … // everything in here acts as that user
}

runas (AuthBootstrap) {
  … // everything in here acts as the platform's ephemeral auth principal
}
```

## Description    {#description}

### Seeing what a user sees   {#visibility}
A row filter like `allow read where Owner == user` means different rows exist for different people. `runas` is how you
observe that:

```osy title="the model: a row filter over the owner" test app=testing-runas
[Principal] entity User {
  [Required] string Name;
}

entity Doc {
  User Owner;
  [MaxLength(200)] string Title;
  security { allow read where Owner == user; }
}

```

The test that observes it, and one detail in it that is not decoration: **`principal` is how a test gets hold of the
person it wants to be.** A `[Test]` body outside a `runas` is an anonymous caller (see [Outside the block](#outside)),
so a `User.Single(…)` written there reads nothing and there is nobody to become. A `principal` declaration resolves
**unsecured**, like the fixture:

```osy title="each user sees only their own rows" run app=testing-runas
principal Alice => User.Single(u => u.Name == "Alice");
principal Bob   => User.Single(u => u.Name == "Bob");

[TestFixture]
void Seed() {
  var alice = new User { Name = "Alice" };
  var bob = new User { Name = "Bob" };
  var aliceDoc = new Doc { Owner = alice, Title = "alice-doc" };
  var bobDoc = new Doc { Owner = bob, Title = "bob-doc" };
}

[Test(Seed)]
void A_user_sees_only_their_own_docs() {
  runas(Alice) {
    Assert.Equal(1, Doc.Count());                          // Bob's row is not merely hidden — it does not exist for her
    Assert.Equal("alice-doc", Doc.Single(d => true).Title);
  }
}
```

Read that first assertion carefully. Under `runas(Alice)`, `Doc.Count()` is **1**. The filter is not a mask applied
after the fact; it is part of the query. Bob's row is not in the result set to be filtered out — it was never in it.

### Proving the denial   {#denial}
Pair `runas` with [`Assert.Denied`](https://osysharp.com/reference/testing/assert/) to claim that someone is refused:

```osy title="the rule denies the person it should" run app=testing-runas
[Test(Seed)]
void Bob_cannot_read_Alices_doc() {
  runas(Bob) {
    Assert.Equal(0, Doc.Count(d => d.Owner == Alice));   // Alice's doc is not his to see
  }
}
```

### Outside the block — the fixture is unrestricted, the test body is NOBODY   {#outside}
These are two different things, and reading them as one is a half-hour lost:

- A **`[TestFixture]`** runs **unsecured**, so it can seed rows across every entity without fighting the rules it is
  about to test.
- A **`[Test]` body** outside a `runas` block runs as **an anonymous caller** — secured, with no principal. Under
  deny-by-default a read there returns nothing, which is why a `Assert.NotNull` on it fails rather than passing on
  unrestricted access.

**Until something signs you in.** Driving the app's own sign-in — `Ui.SignInAs(Alice)`, or filling and submitting
its login form — makes the test body that person for the rest of the test, exactly as it makes the browser that
person. Reads after it are Alice's reads and need no `runas` wrapper; reads written *above* it are still nobody's.
`Ui.SignOut()` takes it away again.

That asymmetry is deliberate, and it is worth stating plainly: **security is only tested inside `runas`, or as
somebody you signed in.** A test that does neither has tested what nobody can do.

### Does a helper need its own runas?   {#helper}
No — **the acting principal flows down the whole call chain.** `runas` rebinds who is asking for the rest of the
block, calls it makes included, so an ordinary function has nothing extra to do: it inherits whoever is running it.

```osy title="a plain helper acts as whoever called it" run app=testing-runas
// AN ORDINARY FUNCTION — not [Test], not [TestFixture], and no runas of its own (writing one here hits the compile
// error above: this is not a [Test] function). It needs none: whoever the CALLER's runas bound is still bound
// while this statement runs.
Doc FindMyDoc(string title) => Doc.FirstOrDefault(d => d.Title == title);

[Test(Seed)]
void A_helper_acts_as_whoever_called_it() {
  runas(Alice) {
    Assert.NotNull(FindMyDoc("alice-doc"));   // her own row filter is evaluated as HER, from inside the helper
  }
  runas(Bob) {
    Assert.Null(FindMyDoc("alice-doc"));      // the SAME helper, called as Bob, sees nothing — RLS still applies
  }
}
```

So a shared helper is written **once**, with no principal ceremony of its own, and **the test switches who is
acting between calls**:

```osy title="two people in one test — the CALLER switches, the helper never knows" syntax
runas(Mia)  { Found(); Invite(); }   // Mia founds the org and sends the invite
runas(Otto) { Accept(); }            // Otto — a DIFFERENT principal — accepts it
```

`Found`, `Invite` and `Accept` each run as whoever called them; none needs a `runas` of its own, and none could
declare one — that stays refused outside a `[Test]` body (see [Why it is test-only](#test-only) below). A helper
that itself needs to act as TWO people within one call is the wrong shape: split it, and let the caller switch
between calls instead, exactly as above.

### Reading a credential, as the auth flow   {#auth-bootstrap}
`runas (AuthBootstrap)` is the one form whose argument is not a row. It becomes the **same user-less ephemeral
principal the platform arms an `[AuthMethod]` with**: no current user, bearing the `[Role]` your
`app.AuthBootstrap` declares, with exactly that role's ordinary `security {}` grants and nothing more.

It exists because a properly-masked credential has exactly one legitimate reader, and until this form a test could not
be it:

```osy title="the mask that leaves a credential one legitimate reader" syntax
security {
  deny read PasswordHash when !IsAuthenticator;   // the shape every app is told to write
  deny read ResetToken   when !IsAuthenticator;
}
```

That mask makes the field unreadable to **every principal a test can name** — which is correct, and which left a test
needing to observe what the auth flow observes with nowhere to stand. The practical result was worse than the gap: an
app whose reset flow had to be tested simply left the token unmasked, and an unmasked credential on the `[Principal]`
rides to the browser on the `Session.CurrentUser` payload.

```osy title="reading that token, as the auth flow itself" syntax
// what the email carried — read as the only thing allowed to see it
string token = "";
runas (AuthBootstrap) { token = User.Single(u => u.Email == "ada@example.com").ResetToken; }
```

⛔ **This peek stands in for the mailbox, and it is honest only if there IS one.** Over an app whose reset flow
really mails the token, reading the column as the auth principal is a fair substitute for opening the mail. Over an
app that mints a token and sends nothing, the identical two lines are a back door: the test completes a flow no user
could, and reports that the flow works. That is not a hypothetical — `demo/auth-demo` shipped exactly that, with
four green tests over it, until 2026-09-04. So pair this peek with something that asserts the token actually LEFT
(the provider's message id on the row, say); `security-reset-token-never-delivered` (MUST) catches the app-shaped
half.

**It grants no new authority.** It is the arming the platform already performs for `Login`, `Signup` and
`PasswordReset`, reachable from a test — so what it can read is what your own auth flow can read, decided entirely by
the grants you wrote. It stays test-only like every other `runas`, and it fails loudly rather than quietly running
anonymously if the app declares no `app.AuthBootstrap` — because a block that reads a masked field while bound to
nobody reads `null`, and an assertion over that would pass for the wrong reason.

## Why it is test-only   {#test-only}
`runas` rebinds the acting user **and that user's roles**. Inside the block, every rule you wrote evaluates for
somebody else. That is exactly what a security test needs and exactly what application code must never be able to do:
it would let any code become any user it could merely *look up*, and on the ordinary shape where signed-in users can
read the user directory, that is everybody.

The platform's promise is that you decide security **once**, on the entity, and it then holds everywhere without you
checking — you never have to ask whether a particular read, function or screen honours it. That promise only survives
while nothing in the language can step around it. So the compiler refuses `runas` outside a `[Test]`:

```text
`runas(...)` is a TEST-ONLY construct and this is not a [Test] function. It rebinds the acting principal and that
principal's roles, which bypasses the security you declared on your entities — so app code may not speak it.
```

**If you need authority before anyone is signed in** — a login, a signup, an OAuth callback, which must read or write
user rows with no user yet — that is [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/), not this. You declare a role and a `policy` that
leashes it, mark the entry points [[security-auth-method|`[AuthMethod]`]], and the platform runs them as an ephemeral
principal bearing that role. The elevation is declared in one place a reviewer can find, bounded by a predicate, and
still not something app code can grant itself.

## See also       {#see-also}
- [Assert](https://osysharp.com/reference/testing/assert/) — `Assert.Denied`, the assertion `runas` makes possible
- [secure by default (deny-all)](https://osysharp.com/reference/security/secure-by-default/) — what is denied before you write any rule
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — the `user` a rule compares against


---

<!-- https://osysharp.com/reference/types/bitwise-operators/ -->

# Bitwise operators

> `&`, `|`, `^`, `~`, `<<` and `>>` work on `int`, with C#'s meanings and C#'s precedence. Unlike `+`, `-`, `*` and `/`, they do not raise on overflow — a bit operation is defined modulo 2^32, so `1 << 31` is a negative number rather than an error. The operands must be integers.

<!-- id: types-bitwise-operators · area: types · stability: stable · html: https://osysharp.com/reference/types/bitwise-operators/ -->

## Summary        {#summary}

Osy# has the whole C# bitwise family — `&` (and), `|` (or), `^` (exclusive or), `~` (complement), `<<` (left shift)
and `>>` (right shift) — together with the compound forms `&=`, `|=`, `^=`, `<<=` and `>>=`.

They work on **`int`**, and they mean exactly what they mean in C#. The two differences worth knowing are both about
what they do *not* do: they never raise on overflow, and they do not accept a `bool`.

## Signature      {#signature}

```osy syntax
int r = (colour >> 16) & 255;      // read one byte out of a packed value
int packed = (r << 16) | (g << 8) | b;   // put three back together

int flags = Read | Write;          // set bits
bool canWrite = (flags & Write) != 0;    // test one
flags &= ~Write;                   // clear it

int doubled = value << 1;          // shift
int halved = value >> 1;           // arithmetic — the sign is preserved
```

## Description    {#description}

### They are unchecked   {#unchecked}

Osy# integer arithmetic is **checked**: `+`, `-`, `*` and `/` raise rather than wrapping to a wrong number when the
result leaves the range of `int`. Bitwise operators are the deliberate exception, because a bit operation is defined
modulo 2^32 rather than as arithmetic on a magnitude.

So `1 << 31` is `-2147483648`, and that is the answer rather than an error — the same as in C#. If it raised, a
perfectly ordinary bit pattern could not be written down.

### The shift count wraps at 32   {#shift-count}

`x << 33` means `x << 1`: the count is masked to its low five bits, as in C#. A shift by a multiple of 32 is
therefore a shift by nothing, not a way to clear a value.

### `>>` keeps the sign   {#sign}

Right shift is *arithmetic*: `-8 >> 1` is `-4`, not a large positive number. The sign bit is copied rather than
zeros being shifted in. This is why the family is defined on `int` and not on a wider or unsigned type — there is
exactly one integer width whose bit behaviour is identical everywhere an Osy# expression can run.

### Precedence is C#'s   {#precedence}

From loosest to tightest:

```text
||   <   &&   <   |   <   ^   <   &   <   ==  !=   <   <  <=  >  >=   <   <<  >>   <   +  -   <   *  /  %
```

Two consequences catch people out in C too, and they are the same here:

- `a & b == c` is `a & (b == c)` — equality binds **tighter** than `&`.
- `1 << 2 + 1` is `1 << 3`, which is `8` — addition binds **tighter** than a shift.

Parenthesise when the reading matters. The compiler will not warn, because the expression is not wrong.

### The operands must be integers   {#operands}

A `double` has no bit pattern the language exposes, so `x & 255` on a double is refused rather than rounded — the
same refusal C# makes.

A **`bool` is also refused**, and here Osy# is narrower than C#. C# lets you write `a & b` on two bools as a
*non-short-circuiting* logical and: both sides are evaluated, and the result is a bool. That is a genuinely different
operation from the integer one, and it is not implemented. It is refused by name rather than quietly treated as
`&&`, which would be the same expression meaning two different things depending on a type you cannot see at the call
site. Use `&&` and `||`.

### Where they run   {#execution-side}

Everywhere. A bitwise expression compiles for a server function, a client action and a query filter alike, and a hot
client region containing one still compiles to JavaScript — JavaScript's bitwise operators are specified over the
same 32-bit conversion, so the compiled form is the operator itself with nothing added.

## Examples       {#examples}

Packing and unpacking a colour, which is what per-pixel graphics code spends its time on:

```osy title="scale the three channels of a packed colour" test app=types-bitwise
int Shade(int colour, int percent) {
  int r = (colour >> 16) & 255;
  int g = (colour >> 8) & 255;
  int b = colour & 255;
  return ((r * percent / 100) << 16) | ((g * percent / 100) << 8) | (b * percent / 100);
}
```

A flags value, set, tested and cleared:

```osy title="flags" test app=types-bitwise
int None() { return 0; }
int Read() { return 1; }
int Write() { return 2; }
int Admin() { return 4; }

int Grant(int flags, int bit) { return flags | bit; }
int Revoke(int flags, int bit) { return flags & ~bit; }
bool Has(int flags, int bit) { return (flags & bit) != 0; }
int Toggle(int flags, int bit) { return flags ^ bit; }
```

Overflow is not an error here, and the sign survives a right shift:

```osy title="the two rules that differ from arithmetic" test app=types-bitwise
int Smallest() { return 1 << 31; }        // -2147483648, not an overflow
int NoOpShift() { return 1 << 32; }       // 1 — the count masks to five bits
int Halve() { return -8 >> 1; }           // -4 — the sign is preserved
```

Both of these are refused:

```osy title="✗ a double has no bits, and a bool wants &&" syntax
double d = 2.5;
int bad = d & 255;        // REFUSED — a double has no bit pattern

bool a = true, b = false;
bool also = a & b;        // REFUSED — use `&&`; C#'s bool `&` is not implemented
```

## See also       {#see-also}
- [Every type, in one list](https://osysharp.com/reference/types/vocabulary/) — the scalar types, and which arithmetic is checked
- [long](https://osysharp.com/reference/types/long/) — the wider integer type, which these operators do not accept
- [Constant expressions](https://osysharp.com/reference/types/constant-expressions/) — where a value has to be known at compile time


---

<!-- https://osysharp.com/reference/types/constant-expressions/ -->

# Constant expressions

> Some places take a value that must be known at compile time — an attribute argument, a config setting, a workflow message. Adjacent string literals joined with `+` are folded before that check, so you can split a long sentence across lines. Anything that is not constant is still refused.

<!-- id: types-constant-expressions · area: types · stability: stable · html: https://osysharp.com/reference/types/constant-expressions/ -->

## Summary        {#summary}

A few places in Osy# take a value that has to be known while the app is being compiled, not while it is running — an
attribute argument, a setting in a config block, an enum member's display label, a workflow message. Those places
require a **constant**.

A string literal is the obvious constant. So is a chain of string literals joined with `+`: the compiler folds
`"Awaiting " + "review"` into `"Awaiting review"` before it checks that the value is constant, exactly as C# does.
That means a long sentence can be split across lines for readability without the compiler objecting.

What is *not* constant is still refused, and for the real reason. A value that depends on a member, a parameter or a
runtime call is not known at compile time, so it cannot go where a constant is required — no matter how it is spelled.

## Signature      {#signature}

```osy syntax
[Label("Awaiting " + "review")]        // folded → "Awaiting review"
[Label("Signed " + "off " + "by legal")]   // any length of chain folds

[Pattern("^DOC-" + Code)]                // REFUSED — `Code` is not a constant
[Pattern($"^DOC-[0-9]+$")]               // REFUSED — an interpolated string is not a literal
```

## Description    {#description}

### Where a constant is required   {#where-required}

These are the places that read a value at compile time rather than evaluating it at run time:

- **Attribute arguments** — `[Label]`, `[Pattern]`, `[ExternalName]`, and a constraint's optional message.
- **Enum member labels** — `[Label]` and `[Icon]` on a member.
- **Config settings** — the values inside a config block, and the entries of a string list.
- **Workflow messages** — a terminal's or a requirement's `Message`.
- **Agent and tool descriptions** — a `Description` on a tool or agent, which the model reads.

In each case the value is baked into the application's metadata when it is compiled. There is no later moment at which
a non-constant could be evaluated, which is why the requirement exists.

### Folding   {#folding}

Folding is *left-nested*, mirroring how `+` associates: `"a" + "b" + "c"` folds only because each step folds. A chain
with one non-constant operand is not a constant at all, and returns nothing rather than a partial result — so
`"Hello " + name` is refused rather than quietly becoming `"Hello "`.

Folding happens during the compile, not in the parsed source. Tooling that reads your code — hover, go-to-definition,
selection — still sees the expression you wrote, with the `+` intact.

### Interpolated strings are deliberately not constants   {#interpolation}

`$"…"` is refused where a constant is required, even when it happens to contain no holes. The `$` prefix declares an
intent to interpolate, and a place that needs a compile-time value should say so plainly rather than accept a form
whose purpose is to be computed. Write a plain literal, or a `+` chain of them.

## Examples       {#examples}

A long label split across two lines, and a three-part chain:

```osy title="splitting a long label" test app=types-constant-expressions
enum ReviewState {
  [Label("Awaiting " + "review")] Pending,
  [Label("Signed " + "off " + "by legal")] Approved,
}
```

An attribute whose pattern *and* whose failure message are both split:

```osy title="a constraint written across lines" test app=types-constant-expressions
entity Doc {
  [Pattern("^DOC-" + "[0-9]+$", "use " + "DOC-1234")] string Code;
  ReviewState State;
}
```

Neither of these is constant, and both are refused:

```osy title="✗ a member reference, and an interpolation" syntax
[Pattern("^DOC-" + Code)]     // depends on a member — not known at compile time
[Pattern($"^DOC-[0-9]+$")]    // an interpolated string is not a literal
```

## See also       {#see-also}
- [Optional and required members](https://osysharp.com/reference/types/optional-and-required/) — how a member's type spelling decides whether a value must be supplied
- [constraints](https://osysharp.com/reference/entity/constraints/) — the attributes that take a pattern and a message
- [enum](https://osysharp.com/reference/enum/declaration/) — where member labels are declared


---

<!-- https://osysharp.com/reference/types/date-and-time/ -->

# DateOnly and TimeOnly

> `DateOnly` is a calendar date (no time); `TimeOnly` is a time of day (no date) — the C# types. Build them with `new DateOnly(y, m, d)` / `new TimeOnly(h, m[, s])`, extract one from a `DateTime` with `FromDateTime`, read parts (`.Year`/`.Month`/`.Day`, `.Hour`/`.Minute`/`.Second`), parse from strings, and store them on an entity.

<!-- id: types-date-and-time · area: types · stability: preview · html: https://osysharp.com/reference/types/date-and-time/ -->

## Summary        {#summary}
`DateOnly` is a **calendar date** with no time component; `TimeOnly` is a **time of day** with no date — the same
split as C#. Both are first-class scalars you can declare, pass, and store on an entity (alongside `DateTime` and
[`TimeSpan`](https://osysharp.com/reference/types/timespan/)).

## Signature      {#signature}
```osy syntax
new DateOnly(year, month, day)                 // a calendar date
new TimeOnly(hour, minute)                      // a time of day
new TimeOnly(hour, minute, second)
DateOnly.FromDateTime(dt) / TimeOnly.FromDateTime(dt)   // the date / time part of a DateTime
d.ToDateTime(t)                                 // …and back: a date + a time of day = a DateTime
DateOnly.Parse(s) / TimeOnly.Parse(s)           // from a string (throws on a bad value)
d.Year / d.Month / d.Day / d.DayOfWeek          // DateOnly parts
d.DayNumber                                     // days since 0001-01-01 — subtract two to count days between
t.Hour / t.Minute / t.Second                    // TimeOnly parts
a < b · a <= b · a > b · a >= b · a == b         // ordering + equality, on two dates or two times of day
```

## Description    {#description}
Construct with the C# spelling — `new DateOnly(2026, 3, 15)` / `new TimeOnly(9, 30)` (a three-argument `TimeOnly`
adds seconds). `FromDateTime` splits a `DateTime` into its date or time half. `Parse` reads one from a string
(invariant format; it throws on an unparseable value). Read the parts with the same member names as C#
(`.Year`/`.Month`/`.Day`; `.Hour`/`.Minute`/`.Second`), and format with `.ToString("fmt")`.

### Which came first?     {#ordering}
Compare them with `<`, `<=`, `>`, `>=` — the ordinary operators, exactly as in C#. Two dates order by calendar day;
two times of day order by clock time. `==` and `!=` compare the same way.

```osy title="a date and a time of day, ordered" test app=date-only-ordering
entity Sighting { [Required] DateOnly Night; [Required] TimeOnly Culmination; }

bool IsInThePast(DateOnly night) {
  return night < DateOnly.FromDateTime(DateTime.UtcNow);
}

bool IsBeforeDawn(TimeOnly culmination) {
  return culmination < new TimeOnly(6, 0);
}
```

Compare **like with like**: two `DateOnly`s, or two `TimeOnly`s. A `DateOnly` beside a `DateTime` is a different
question and the compiler refuses the pairing rather than guessing which one you meant — take the date out of the
`DateTime` first with `FromDateTime`.

The same comparison works inside a query, where it becomes part of the SQL:
`Sighting.Where(s => s.Night >= today)`.

### How many days between two dates?     {#days-between}
Subtract their `DayNumber`s — C#'s own spelling, and the only one there is:

```osy test app=date-only-days-between
entity Item { [Required, MaxLength(60)] string Name; [Required] DateOnly AddedOn; }

int DaysIn(DateOnly addedOn) {
  return DateOnly.FromDateTime(DateTime.UtcNow).DayNumber - addedOn.DayNumber;
}
```

`DayNumber` is the count of days since 0001-01-01, so the difference between two of them is the number of days
between the two dates. **Two dates cannot be subtracted directly** — a `DateOnly` carries no time, so there is no
duration between them, and C# has no such operator either. When you do want a duration with hours and minutes in
it, subtract two `DateTime`s instead: that gives a [`TimeSpan`](https://osysharp.com/reference/entity/properties/).

⚠ A date read out of a `DateTime` counts the same as the date itself: `DayNumber` is taken from the calendar day,
so a value stamped at 23:59 and one stamped at 00:01 the same morning answer the same number.

Both are computed **in memory** and stored as ordinary columns (a `DateOnly` as a SQL `date`, a `TimeOnly` as a
`time`).

## Examples       {#examples}
```osy title="construct, convert, and read parts" test app=date-and-time
DateOnly Christmas() {
  return new DateOnly(2026, 12, 25);
}

int MeetingHour() {
  var t = new TimeOnly(9, 30, 0);
  return t.Hour;                                 // 9
}

DateOnly DatePart(DateTime dt) {
  return DateOnly.FromDateTime(dt);              // the calendar date, time dropped
}

int MinuteOf(DateTime dt) {
  return TimeOnly.FromDateTime(dt).Minute;
}
```

A `DateOnly` / `TimeOnly` is also an **ordinary entity column** — no conversion to write, and no `DateTime` to fall
back on. This example is COMPILED by the docs gate, so the claim is checked rather than asserted:

```osy title="the same two types as entity columns" test app=date-and-time
entity Appointment {
  DateOnly Day;                  // a SQL `date` — no time of day to get wrong
  TimeOnly StartsAt;             // a SQL `time` — no date attached
}

DateOnly DayOf(Appointment a) { return a.Day; }        // reads back as itself, not as a DateTime
TimeOnly StartOf(Appointment a) { return a.StartsAt; }
```

## See also       {#see-also}
- [entity members](https://osysharp.com/reference/entity/properties/) — every type an entity member may hold, in one table
- [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/) — durations, and `DateTime`/`TimeSpan` arithmetic
- [Current time (DateTime.UtcNow, DurableClock.Now)](https://osysharp.com/reference/function/current-time/) — reading the clock (`DateTime.UtcNow`), parsing, and formatting


---

<!-- https://osysharp.com/reference/types/datetime/ -->

# DateTime

> A date and time. It is a wall-clock value, not an instant on a timeline, so it is never shifted by anybody's timezone. Reading its parts and doing calendar arithmetic on it work identically in the browser and on the server; reading the current time is a server operation.

<!-- id: types-datetime · area: types · stability: stable · html: https://osysharp.com/reference/types/datetime/ -->

## Summary        {#summary}
`DateTime` is a date and a time of day — `2024-03-15T13:45:30` — held exactly, to 100 nanoseconds. You read its parts
(`d.Year`, `d.DayOfWeek`), do calendar arithmetic on it (`d.AddMonths(1)`), and construct or parse one
(`new DateTime(2024, 3, 15)`, `DateTime.Parse(s)`).

## Signature      {#signature}
```osy syntax
new DateTime(<int> year, <int> month, <int> day)
new DateTime(<int> year, <int> month, <int> day, <int> hour, <int> minute, <int> second)
DateTime.Parse(<string> s) -> DateTime

DateTime.MinValue -> DateTime         // the earliest representable value
DateTime.MaxValue -> DateTime         // the latest representable value

d.Year · d.Month · d.Day · d.Hour · d.Minute · d.Second · d.DayOfWeek · d.Date
d.AddDays(n) · d.AddMonths(n) · d.AddYears(n) · d.AddHours(n) · d.AddMinutes(n)
```

## Description    {#description}

### What are `DateTime.MinValue` and `MaxValue`? {#range-endpoints}
`DateTime.MinValue` and `DateTime.MaxValue` are the earliest and latest values a `DateTime` can hold. They are
ordinary values, so they compare and sort like any other date — which is what makes them useful as a starting point
for a running comparison (`var earliest = DateTime.MaxValue;` then keep the smaller of each candidate).

⚠ **They are not a "no date" marker.** A `DateTime?` says "no date" precisely and reads as `null`; a sentinel says
it by convention and every reader has to know the convention. Prefer the nullable type — a `MinValue` that leaks
into a UI renders as a real date in the year 1, and a `MinValue` that reaches a comparison silently sorts first.

### It is a wall-clock value, not an instant   {#wall-clock}

`2024-03-15T13:45:30` means *that reading on a clock face*. It is not "a moment in time as seen from a timezone", and
nothing in the platform will shift it by one. A date you store is the date you get back — the same one, in the same
digits, whether it is read on a server in Frankfurt or in a browser in São Paulo.

That is worth stating plainly because most date libraries do the opposite, and quietly.

### Calendar arithmetic clamps the day   {#clamping}

`AddMonths` and `AddYears` move along the **calendar**, and clamp the day to the target month rather than overflowing
it:

```osy syntax
new DateTime(2024, 1, 31).AddMonths(1)     // 2024-02-29  — a leap year
new DateTime(2025, 1, 31).AddMonths(1)     // 2025-02-28
new DateTime(2024, 2, 29).AddYears(1)      // 2025-02-28
```

None of those becomes March 2nd. If you want exactly thirty days later, say `AddDays(30)`.

`AddDays`, `AddHours` and `AddMinutes` take a fractional amount and round it to the nearest millisecond, so
`AddDays(0.5)` is exactly twelve hours.

### DayOfWeek counts from Sunday   {#day-of-week}

`d.DayOfWeek` is `0` for Sunday through `6` for Saturday.

### Reading the current time   {#now}

`DurableClock.Now`, `DurableClock.UtcNow` and `DurableClock.Today` give the current instant, and they run **on the client** — reading
"now" costs no round trip. That is safe here for a reason worth knowing: a `DateTime` is a UTC *instant*, not a
wall-clock reading, so the browser and the server name the same value (9am in Frankfurt *is* 5pm in Tokyo). Test
pinning still applies, and a durable flow that pauses and resumes still sees the instant it saw before, because the
engine resumes from a saved point rather than re-running the body from the top.

⚠ **`DateTime.Now` and `DateTime.UtcNow` are the same instant**, because there is no local-time `DateTime` here for
them to differ by. Local is a question about a person, not a property of the platform — so when you want a wall
clock, name the zone: `DateTime.UtcNow.InZone(Zone.Of("Europe/Stockholm"))`.

Everything else about a date — its parts, its arithmetic, parsing and formatting it — runs wherever you are, with no
round trip.

## Examples       {#examples}
```osy title="a due date, and whether it has passed" test app=text-search
bool IsOverdue(DateTime due) {
  return due < DurableClock.UtcNow;
}

DateTime NextBillingDate(DateTime start) {
  return start.AddMonths(1);
}
// NextBillingDate(new DateTime(2024, 1, 31))  ->  2024-02-29
```

## See also       {#see-also}
- [decimal](https://osysharp.com/reference/types/decimal/) — the other exact value type, and the same reasoning behind it
- [format specifiers](https://osysharp.com/reference/function/format-specifiers/) — rendering a date or a number to a string
- [execution side](https://osysharp.com/reference/function/execution-side/) — why the clock is a server operation and the arithmetic is not


---

<!-- https://osysharp.com/reference/types/vocabulary/ -->

# Every type, in one list

> The complete vocabulary of built-in types — the scalars you can store, the collections, the two callable spellings (`Action` and `Func`), and the wrappers a component parameter may take. If a type is not on this page and you did not declare it yourself, it does not exist.

<!-- id: types-vocabulary · area: types · stability: stable · html: https://osysharp.com/reference/types/vocabulary/ -->

## Summary        {#summary}
[Types](https://osysharp.com/reference/types/index/) explains the types with a wrinkle. This page is the other half: the **complete list**, so that
"does the language have a type for this?" is a question you can answer by looking rather than by guessing.

It is worth having because guessing goes wrong in a specific way — you invent a name for something that already
exists. There is no `Command` type for a callable; it is `Action`. There is no `Set<T>`; it is `HashSet<T>`. Every
name below is one the compiler matches, and the list is checked against the compiler itself, so it cannot quietly
fall behind.

Anything not on this page is a type **you** declare — an `entity`, an `enum`, a `class`, or a component's own type
parameter.

## Description    {#description}

### Scalars and value kinds    {#scalars}
The storable types. These are what an entity member, a function local, or a component parameter may be.

| Type | What it is |
|---|---|
| `string` | Text. `default(string)` is null, exactly as in C#. |
| `char` | A single character, in single quotes. See [char](https://osysharp.com/reference/types/char/). |
| `int` | A 32-bit integer. |
| `long` | A 64-bit integer. See [long](https://osysharp.com/reference/types/long/). |
| `double` | A double-precision float — measurements, science. |
| `bool` | True or false. |
| `decimal` | Exact base-10 — money, quantities. See [decimal](https://osysharp.com/reference/types/decimal/). |
| `DateTime` | A date and time. See [DateTime](https://osysharp.com/reference/types/datetime/). |
| `DateOnly` | A calendar date with no time of day. |
| `TimeOnly` | A time of day with no date. |
| `TimeSpan` | A duration. See [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/). |
| `Guid` | A globally unique id. |
| `Json` | An arbitrary JSON document. |
| `RichText` | Formatted prose, stored as a rich-text document. |
| `Markdown` | A section-addressable markdown document. See [Markdown](https://osysharp.com/reference/types/markdown/). |
| `Vector` | An embedding, for similarity search; `[MaxLength]` sets the dimensions. |
| `Zone` | An IANA time-zone token, e.g. `Europe/Stockholm`. |
| `Culture` | A BCP-47 culture token, e.g. `sv-SE`. |

A bare member of one of these is **required** unless the type has an honest zero — see
[Optional and required members](https://osysharp.com/reference/types/optional-and-required/), which is the rule that decides whether `?` is needed.

### Which collection types may I write?    {#collections}

| You write | What it is |
|---|---|
| `T[]` | An array. |
| `T[][]` | A JAGGED array — a collection of collections, which is how you spell a grid. Indexes as `g[y][x]`, on both sides of an assignment. `T[,]` (rectangular) is not a type here, and the compiler says so and points at this form. |
| `byte[]` | Binary data — the one array form that is a scalar rather than a collection. |
| `List<T>` | An ordered, mutable list. |
| `HashSet<T>` | A set of distinct values. |
| `Dictionary<K, V>` | A keyed map. |
| `stream<T>` | A collection that is still being written. A function returning one produces its results with `yield return`, and a `live var` bound to it renders each item as it arrives — see [yield — a function that produces results over time](https://osysharp.com/reference/function/yield/). |

An entity's **child rows** are not one of these — they are a collection property on the parent, described in
[relations](https://osysharp.com/reference/entity/relations/). Reach for `List<T>` for an in-memory list, never to hold children.

**A jagged array is not a separate kind.** `T[]` *is* a collection of `T`, so `T[][]` is a collection of those.
The outer rank is a `List`, so a grid **can be appended to** (`board.Add(row)`) and a `List<T[]>` goes straight into
one — a bare `T[]` value does not, for the reason every `T[]` → `List<T>` is refused: an array cannot promise the
`.Add` the slot offers. So build the grid as a `List<T[]>` and assign it as it stands, without a `.ToArray()`.

```osy title="a grid — build it, read it, write it" test app=text-search
int[][] Grid(int w) {
  var rows = new List<int[]>();
  for (var y = 0; y < w; y = y + 1) {
    var row = new List<int>();
    for (var x = 0; x < w; x = x + 1) { row.Add(0); }
    rows.Add(row.ToArray());
  }
  int[][] board = rows;                     // the List goes straight in — no `.ToArray()` on the outer rank
  board[0][0] = 1;                          // write a cell
  return board;
}

int[][] Laid() { int[][] board = [[1, 2], [3, 4]]; return board; }   // …or laid out literally
```

⚠ **`int[,]` does not exist.** A rectangular array is a distinct type in C# and not one Osy# has; the compiler
refuses it by name and tells you to write `int[][]`, which indexes identically.

### How do I type a function value?    {#callables}
Two spellings, both exactly C#'s, and there are no others:

| You write | What it is |
|---|---|
| `Action` | A callback that takes nothing and returns nothing. |
| `Action<T…>` | A callback that takes arguments and returns nothing. |
| `Func<T…, TResult>` | A callback that returns a value. The **last** type argument is the return type. |

```osy title="declaring a callback parameter and a member" syntax
component PrimaryButton(string label, Action onPress) { … }

class MenuEntry {
  public string Label;
  public Action Run;          // the verb to run — an Action, not a "Command"
}
```

`Func` with no type argument is an error: a function that returns something must say what. For a callback that
returns nothing, use `Action`.

**Calling one.** A callable is invoked exactly as in C# — `run()` on a local or parameter, `entry.Run()` on a class
member, and `Run()` unqualified inside the class that declares it:

```osy title="invoking one — on a parameter, and on a member" syntax
class MenuEntry {
  public string Label;
  public Action Run;
}

component Menu(MenuEntry[] entries, Action onDismiss) {
  action Choose(MenuEntry entry) {
    entry.Run();      // run the verb the caller put on this entry
    onDismiss();      // and the callback this component was given
  }
  …
}
```

The argument count and types are checked against the callable's signature, so `Action<int>` invoked with a string is
a compile error rather than a surprise on the client.

**A verb call evaluates to nothing.** It is fire-and-forget: the call does not wait for the verb to finish and yields
no value, so a `Func<…, T>` cannot be invoked — the compiler says so by name rather than handing you a value that
never arrives. Use `Action` for a callback and a plain function when you want a result.

### Component-parameter wrappers    {#component-props}
Writable on a `component` parameter, where they mean something the plain type cannot say:

| You write | What it is |
|---|---|
| `Binding<T>` | A two-way binding — the component **reads and writes** the caller's value. |
| `Query<T>` | A reactive query handle the component re-reads as the data changes. |
| `Content` | An opaque children slot — whatever the caller nests inside. |
| `Slot` | A named children slot. See [Slot (child content)](https://osysharp.com/reference/ui/slots/). |

```osy syntax
component TypeDropdown(Binding<OrganizationType> value) { … }
```

### Types you declare    {#declared}
Everything else is yours: an `entity` (persisted), an `enum`, a plain `class` (in-memory), and a component's own
type parameters. Those are named by their declaration and scoped by [namespace](https://osysharp.com/reference/types/namespace/) and [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/).

## Examples       {#examples}

Scalars and a collection, together in one function, so the spellings on this page are shown compiling rather than
only described. (Callables belong to a component or a class, so their example lives in **Callables** above.)

```osy title="scalars and a collection" test app=text-search
string Summarise(string title, List<decimal> amounts) {
  decimal total = 0;
  foreach (var a in amounts) {
    if (a > 0) total = total + a;
  }
  return title + ": " + total;
}
// Summarise("Q1", [10, -5, 20])   ->  "Q1: 30"
```

## See also       {#see-also}
- [Types](https://osysharp.com/reference/types/index/) — the types with a wrinkle worth reading about first
- [Optional and required members](https://osysharp.com/reference/types/optional-and-required/) — which bare members are required, and when `?` is needed
- [entity members](https://osysharp.com/reference/entity/properties/) — declaring these as entity members
- [component](https://osysharp.com/reference/ui/component/) — where `Binding<T>`, `Query<T>`, `Content` and `Slot` are used
- [Classes](https://osysharp.com/reference/class/index/) — plain in-memory value shapes


---

<!-- https://osysharp.com/reference/types/json/ -->

# Json

> A member that holds a JSON document — an object, an array, or any value. It is stored as real jsonb, and written and read through JsonSerializer, so any value you can serialize goes in. Reach for it when the shape genuinely varies; declare real properties when it does not.

<!-- id: types-json · area: types · stability: stable · html: https://osysharp.com/reference/types/json/ -->

## Summary        {#summary}
A **`Json`** member holds a JSON document — an object, an array, a number, whatever the row needs. It is stored as
**real `jsonb`**, so the database holds structure rather than a blob of text.

```osy syntax
entity Job {
  [Required, MaxLength(50)] string Name;
  Json Detail;                              // whatever this job's handler wants to record
}
```

There are two ways in, and the shape of your data picks between them. When the document has a shape worth naming,
declare a `class` and use [JsonSerializer](https://osysharp.com/reference/json/serializer/)'s `Serialize` — it takes **any value**: a scalar, a list, a map, a class
instance, an entity. When the shape is decided at the call site, write it inline as `new { … }`, which is itself a
`Json` value. Either way you read it back with `Deserialize`.

## Signature      {#signature}
```osy syntax
Json  Detail;     // required — a JSON document has no honest empty value
Json? Detail;     // optional — absent until something writes it
```

## Description    {#description}

### How do I write a document and read it back?     {#round-trip}
The pair is the whole surface, and it is the ordinary C# shape. A `class` is the usual choice when the document has a
shape worth naming — but it is a choice, not a requirement:

```osy title="serialize a class in, deserialize it back out" syntax
class Detail { public int Attempt; public string Reason; }

entity Job { [Required, MaxLength(50)] string Name; Json Detail; }

void Record() {
  var j = new Job { Name = "j1", Detail = JsonSerializer.Serialize(new Detail { Attempt = 2, Reason = "retry" }) };
}

int Attempts(Job job) {
  return JsonSerializer.Deserialize<Detail>(job.Detail).Attempt;
}
```

**Anything serializable goes in**, not just a class:

```osy title="an array, a scalar, an entity — all go in" syntax
Json Tags    = JsonSerializer.Serialize(["urgent", "billing"]);   // an array
Json Reading = JsonSerializer.Serialize(42);                       // a scalar
Json Row     = JsonSerializer.Serialize(order);                    // an entity, as its shallow shape
```

⚠ **The compiler does not check the text, but the database does.** A `Json` column is `jsonb`, so Postgres rejects
anything that is not well-formed JSON — the failure arrives at the WRITE, not at the compile. Assembling the text by
hand is therefore not merely awkward, it is how you get a runtime error out of a program that compiled; `Serialize`
cannot produce malformed output.

### When the shape is decided at the call site: `new { … }`     {#object-literal}
A `class` is the right answer when the document has a shape worth naming and reusing. When it does not — a diagnostic
detail, a webhook body you are assembling, one row's worth of context — write the document inline:

```osy title="a shape with no name, written at the call site" syntax
entity Job { [Required, MaxLength(50)] string Name; Json Detail; }

void Record(int attempt, string reason) {
  var j = new Job { Name = "j1", Detail = new { attempt = attempt, reason = reason, at = DateTime.UtcNow } };
}
```

A name can be left out only where the value supplies one — `new { v.Label }` means `Label = v.Label`. A bare local
does not, and the compiler says so rather than inventing a key.

`new { … }` **is** a `Json` value — Osy# has no anonymous types, it has a document type — so it goes anywhere a
`Json` is accepted: a property, a local, an argument, a return.

**Documents nest**, and a nested one stays a document rather than becoming a quoted string:

```osy title="a nested document stays a document" syntax
Json outer = new { code = "E17", inner = new { retries = 3, fatal = false } };
// → {"code":"E17","inner":{"retries":3,"fatal":false}}
```

Two things the compiler refuses, both because a document has one value per key: **a repeated key**
(`new { a = 1, a = 2 }`), and **the dictionary spelling** (`new { ["a"] = 1 }` — a document key is a name).

⚠ **It is not an escape from the type system.** A document is a `Json` value and nothing else, so
`Runs = new { a = 1 }` on an `int` column is refused exactly as any other type mismatch would be.

### It is required by default, like every type with no honest zero     {#required}
A number defaults to `0` and a `bool` to `false`, and those are real answers. A document has no equivalent, so a bare
`Json` member is **required** and a `new Job { … }` that omits it is refused. Write `Json?` when a job may genuinely
not have one yet — see [Optional and required members](https://osysharp.com/reference/types/optional-and-required/).

### How do I cap how big a document may get?     {#size}
A JSON member has no natural limit, so `[MaxBytes]` is how you give it one:

```osy syntax
[MaxBytes(1048576)] Json Payload;      // at most 1 MB of stored JSON
```

Worth doing on anything an outside system writes into. See [constraints](https://osysharp.com/reference/entity/constraints/).

### When NOT to use it     {#when-not}
The storage is structured, but **Osy# has no way to reach inside it**: you cannot filter, sort or group by something
within the document, secure a field of it, or bind one to a grid column. What you can do is read the whole document
out and deserialize it. So a `Json` member is not a shortcut for properties you were going to query.

Declare real properties whenever the shape is known — even a long one. Reach for `Json` when it genuinely varies per
row: a webhook body you received, a provider-specific configuration block, a handler's own diagnostic detail. If you
find yourself deserializing to the same class everywhere and filtering its fields in memory, those fields wanted to be
properties.

## Examples       {#examples}

Recording a provider-specific configuration whose shape differs per provider, with the known parts declared and only
the varying part left as a document:

```osy title="provider-config" test app=types-json-example
enum Provider { GitHub, GitLab }

class GitHubConfig { public string Owner; public string Repo; public bool UseChecks; }

entity Connection {
  [Required, MaxLength(100)] string Name;
  Provider Provider;                             // declared — queried, filtered, shown
  [MaxBytes(65536)] Json ProviderConfig;         // varies per provider
}

void ConnectGitHub(string name, string owner, string repo) {
  var c = new Connection {
    Name           = name,
    Provider       = Provider.GitHub,
    ProviderConfig = JsonSerializer.Serialize(new GitHubConfig { Owner = owner, Repo = repo, UseChecks = true }),
  };
}
```

`Provider` is a real property because every connection has one and you will filter on it. `ProviderConfig` is a
document because GitHub's settings and GitLab's have nothing in common.

The same job when the shape is one call site's business — a failure record nobody else consumes, so declaring a class
for it would be ceremony:

```osy title="inline-document" test app=types-json-inline
entity Delivery {
  [Required, MaxLength(100)] string Endpoint;
  int Attempts;
  Json? LastFailure;                             // absent until something goes wrong
}

void RecordFailure(string endpoint, int status, string body) {
  var d = Delivery.Where(x => x.Endpoint == endpoint).First();
  d.Attempts = d.Attempts + 1;
  d.LastFailure = new {
    status  = status,
    body    = body,
    attempt = d.Attempts,
    context = new { endpoint = endpoint, at = DateTime.UtcNow },
  };
}
```

`context` nests as a document, not as a quoted string — so a consumer reading `LastFailure` back gets structure all
the way down.

## See also       {#see-also}
- [JsonSerializer](https://osysharp.com/reference/json/serializer/) — `Serialize` / `Deserialize`, which are how a `Json` member is written and read
- [Markdown](https://osysharp.com/reference/types/markdown/) — the other member type whose value is a document rather than a scalar
- [Optional and required members](https://osysharp.com/reference/types/optional-and-required/) — why a bare `Json` is required, and when to write `Json?`
- [constraints](https://osysharp.com/reference/entity/constraints/) — `[MaxBytes]`, for bounding a document's stored size
- [entity members](https://osysharp.com/reference/entity/properties/) — declaring members in general


---

<!-- https://osysharp.com/reference/types/markdown/ -->

# Markdown

> A member that holds a markdown document. You read and write it as ordinary text, but it is stored as a list of sections split on its headings, so a person and an agent can edit different parts of the same document without overwriting each other. A markdown member is never required, and one that has never been written reads back null.

<!-- id: types-markdown · area: types · stability: stable · html: https://osysharp.com/reference/types/markdown/ -->

## Summary        {#summary}

`Markdown` is the type for a member that holds a **document** rather than a line of text. You use it exactly like a
string — assign markdown to it, read markdown back — but it is not stored as one blob. The platform splits the text on
its headings and keeps one row per section.

That storage is what buys you the behaviour you actually want from a document:

- **Two writers can work at once.** A person editing one section and an agent rewriting another do not collide,
  because they are writing different rows.
- **An edit costs what the edit is worth.** Ticking a checkbox rewrites one section, not the whole document.
- **Search sees sections, not files.** With `[Searchable]`, each section is indexed on its own, so a search result
  points at the part that answered rather than at a ten-page document.

You do not have to think about any of that to use one. Assign a string, read a string.

## Signature      {#signature}

```osy syntax
entity Article {
  Markdown Body;            // document-backed; reads null until something writes it
  Markdown? Notes;          // `?` is accepted but changes nothing — see below
  [Searchable] Markdown Manual;   // each SECTION is indexed separately
}
```

## Description    {#description}

### Reading and writing   {#read-write}

A markdown member reads and writes as text. Assigning replaces the whole document; reading returns the document
reassembled in order.

```osy title="write it, read it" test app=types-markdown
entity Article {
  [MaxLength(200)] string Title;
  Markdown Body;
}

void PublishDraft(string title) {
  var a = new Article {
    Title = title,
    Body  = "# Overview\n\nWhat this is about.\n\n## Details\n\nThe specifics."
  };
}

string ReadBody(Article a) {
  return a.Body;
}
```

The document written above is stored as **two** sections — `Overview` and `Details` — because those are its two
headings. `ReadBody` returns the text you wrote, reassembled from them.

### A markdown member is never required   {#optional}

Most value-shaped types with no natural zero — `string`, an `enum`, `DateTime`, `Guid` — are **required**: you must
supply a value before a row can be saved (see [Optional and required members](https://osysharp.com/reference/types/optional-and-required/)). **`Markdown` is not one of them.**

A document that has never been written to genuinely is not there, and there is nothing you could "supply" at create
time that would make it there. So a bare `Markdown Body;` is optional, and a row that never touches it saves fine:

```osy title="a row with an untouched document saves normally" test app=types-markdown-optional
entity Page {
  [MaxLength(200)] string Title;
  Markdown Body;
}

void CreateEmptyPage(string title) {
  var p = new Page { Title = title };   // Body is never set — this is fine
}
```

Reading `p.Body` afterwards returns `null`, not an empty string: nothing has been written, and the platform does not
invent a document to hand you. Writing `Markdown? Body;` is accepted and means the same thing — the `?` is redundant
here rather than wrong.

### Assigning null clears it   {#null}

`a.Body = null;` empties the document — it removes its sections. It does not delete the row that owns it.

### It has no column of its own   {#storage}

A markdown member is stored in its own section rows, keyed by the owning row and the member's name, so it adds no
column to its entity's table. Two consequences worth knowing:

- **Do not filter on it in a query.** There is no column to compare against, so `Where(a => a.Body.Contains("x"))` is
  not the way to find text. Use `[Searchable]` and search it — that is what section-level indexing is for.
- **Deleting the row deletes its document.** You do not clean it up yourself.

### Searching it   {#searchable}

`[Searchable]` on a markdown member indexes **each section separately**, which is almost always what you want from a
long document — a hit points at the section that matched.

```osy title="indexing it, one section at a time" syntax
entity Manual {
  [Searchable] Markdown Body;     // sections indexed individually
}
```

See [[Searchable]](https://osysharp.com/reference/memory/searchable/) for how the results are queried.

## Examples       {#examples}

```osy title="a knowledge-base article, written and then appended to" test app=types-markdown-kb
entity Kb {
  [MaxLength(200)] string Title;
  Markdown Body;
}

void Seed(string title) {
  var k = new Kb { Title = title, Body = "# Intro\n\nStart here." };
}

void Rewrite(Kb k, string body) {
  k.Body = body;
}
```

## See also       {#see-also}
- [Optional and required members](https://osysharp.com/reference/types/optional-and-required/) — why most no-natural-zero types are required, and why this one is not
- [[Searchable]](https://osysharp.com/reference/memory/searchable/) — indexing a document so its sections can be searched
- [entity](https://osysharp.com/reference/entity/declaration/) — declaring the entity a markdown member lives on


---

<!-- https://osysharp.com/reference/types/optional-and-required/ -->

# Optional and required members

> A member is required or optional by how you spell its type. A bare value type with a natural zero reads that zero; a bare value-shaped type with no natural zero (string, enum, date, id) is REQUIRED — you must give it a value. Add `?` to make any member optional (it reads back null). Entity references are the one exception: a bare reference is optional, and you write `[Required]` to demand it.

<!-- id: types-optional-and-required · area: types · stability: stable · html: https://osysharp.com/reference/types/optional-and-required/ -->

## Summary        {#summary}

Whether a member is required or optional is carried by its **type spelling** — there is no separate keyword to
remember. A bare member is non-nullable; the `?` suffix makes it optional (it reads back `null`). What "bare" *means*
depends on whether the type has a natural zero:

- A value type with a **natural zero** — `int`, `long`, `double`, `decimal`, `bool`, `TimeSpan` — reads that zero when
  left unset. `int Count;` reads back `0`, exactly as a C# field would.
- A value-shaped type with **no natural zero** — `string`, any `enum`, `DateTime`/`DateOnly`/`TimeOnly`, `Guid`,
  `Json` — is **required**: the platform invents no value for it, so you must supply one before the row is saved.
- An **entity reference** is the exception: a bare reference is **optional**. Write `[Required]` to demand it.
- A **`Markdown`** member is also optional, for a different reason: it holds a document stored as its own sections, so
  a document nobody has written simply is not there and there is nothing to supply. See [Markdown](https://osysharp.com/reference/types/markdown/).

## Signature      {#signature}

```osy syntax
entity Ticket {
  string Title;                 // REQUIRED  — a bare string has no natural zero, so you must set it
  string? Note;                 // optional  — reads back null when unset
  int Priority;                 // reads back 0 (int has a natural zero)
  Status State;                 // REQUIRED  — an enum has no natural zero
  Status State2 = Status.Open;  // optional-with-a-default — the default supplies the value
  DateTime? ResolvedAt;         // optional  — null until it is resolved

  Customer Reporter;            // OPTIONAL  — references are optional by default
  [Required] Team Team;         // required  — opt a reference in with [Required]
}
```

## Description    {#description}

### Three ways to spell a member   {#spellings}

Every member falls into one of three shapes:

1. **Bare** (`string Title;`) — non-nullable. For a natural-zero type this reads the zero; for everything else it is
   **required**.
2. **Optional** (`string? Note;`) — nullable. Reads back `null` when nobody set it.
3. **Defaulted** (`Status State = Status.Open;`) — non-nullable, but you supplied the value once at the declaration,
   so the row is never without one.

### Why a bare `string` is required   {#bare-string}

In C#, `default(string)` is `null`, not `""`. An empty string is a *value someone typed*, not the absence of one —
conflating them hides bugs. So Osy# does not invent an empty string for you: a bare `string Title;` must be given a
value by the time the row is saved. If you genuinely want "maybe unset," say so with `string? Title;`, which reads back
`null`. The same reasoning covers `DateTime`, `Guid`, and `Json` — there is no honest "zero" of those types to fall
back on.

### Enums are required, not silently first   {#enums}

A bare `enum` is **required**. It is *not* quietly defaulted to whichever member you happened to list first —
reordering the members would silently change the stored default, and a value at position zero may not even be a member
you named. Give it an explicit default when you want one (`Status State = Status.Open;`), make it optional
(`Status? State;`), or supply it before the row is saved.

### References are optional by default   {#references}

An entity reference is the deliberate exception. The everyday shape is: create the row, *then* let the user pick the
related record from a dropdown — so demanding the reference at creation would be wrong. A bare `Customer Reporter;` is
therefore **optional** (reads back `null`). When a reference truly must be present, mark it `[Required] Team Team;`.

### When a required member is checked   {#when-checked}

"Required" means *present when the value becomes real* — and that moment depends on what you are building:

- **Entities are saved**, so a required entity member is checked when you **commit** the row. This is what makes the
  everyday create-form work: seed an empty draft (`draft = new Ticket {}`), bind each field to an input, and let the
  user fill it — the required members are checked when the row is saved, not while it is still being typed. Commit a row
  with a required member still unset and it is refused, naming the member.
- **A `class` is a transient value** with no save step, so its required members are checked at **construction** — the
  compiler stops you at `new`, naming the member and the three fixes:

  ```osy syntax
  new Receipt { }         // error: 'Receipt.Number' must be given a value — set it here
                          //        (new Receipt { Number = … }), give it a default (Number = …;),
                          //        or make it optional (string? Number;).
  ```

## Examples       {#examples}

A bare `string` parameter is required; the `?` suffix makes one optional and it reads back `null` — the same spelling
rule that governs entity members, shown here on ordinary values so it compiles on its own.

```osy title="a required name and an optional nickname" test app=text-search
string DisplayName(string name, string? nickname) {
  // `name` is required (a bare string); `nickname` is optional (`?`), so it may be null.
  return nickname == null ? name : name + " (" + nickname + ")";
}
// DisplayName("Ada", null)   ->  "Ada"
// DisplayName("Ada", "Countess")   ->  "Ada (Countess)"
```

## See also        {#see-also}

- [DateTime](https://osysharp.com/reference/types/datetime/) — a bare `DateTime` is required; use `DateTime?` for "unset until it happens".
- [enum](https://osysharp.com/reference/enum/declaration/) — declaring enums and their defaults.
- [entity](https://osysharp.com/reference/entity/declaration/) — declaring entities, references, and `[Required]`.


---

<!-- https://osysharp.com/reference/types/timespan/ -->

# TimeSpan (durations)

> `TimeSpan` is the duration type — a length of time, as in C#. Build one with the `TimeSpan.FromX` factories or `new TimeSpan(…)`, read `.TotalHours`/`.TotalMinutes`/… (fractional) or `.Days`/`.Hours`/… (whole components), and store it on an entity like any scalar. Evaluated in memory.

<!-- id: types-timespan · area: types · stability: preview · html: https://osysharp.com/reference/types/timespan/ -->

## Summary        {#summary}
`TimeSpan` is a **duration** — a length of time — exactly as in C#. It is a first-class scalar: you can declare
a `TimeSpan` local, pass it to a function, and **store it on an entity** (persisted as an interval). Build one
with the `TimeSpan.FromX` factories or the `new TimeSpan(…)` constructor; read its total or component parts
through members.

## Signature      {#signature}
```osy syntax
TimeSpan.FromDays(n) / FromHours(n) / FromMinutes(n) / FromSeconds(n) / FromMilliseconds(n)   // factories
new TimeSpan(hours, minutes, seconds)              // constructor (h/m/s)
new TimeSpan(days, hours, minutes, seconds)        // constructor (d/h/m/s)
ts.TotalDays / TotalHours / TotalMinutes / TotalSeconds / TotalMilliseconds   // fractional whole-span totals (double)
ts.Days / Hours / Minutes / Seconds                // whole component parts (int)
```

## Description    {#description}
A factory takes a numeric count and returns the duration: `TimeSpan.FromHours(2)` is two hours. The constructor
takes whole components — `new TimeSpan(1, 30, 0)` is one hour thirty minutes (h, m, s), and the four-argument
form leads with days.

The **`.TotalX`** members give the whole span measured in that unit, as a fractional `double` — `TimeSpan.FromMinutes(90).TotalHours`
is `1.5`. The **component** members (`.Days`, `.Hours`, `.Minutes`, `.Seconds`) give the whole-number parts of the
breakdown — for `new TimeSpan(1, 2, 30, 0)`, `.Days` is `1` and `.Hours` is `2`.

A `TimeSpan` entity property is stored and read back faithfully:

```osy title="a duration on an entity, and the arithmetic over it" test app=types-timespan
entity WorkItem {
  [Required, MaxLength(200)] string Title;
  TimeSpan Estimate;
}

TimeSpan DefaultEstimate() {
  return new TimeSpan(1, 30, 0);          // 1h30m — the (h, m, s) constructor
}

double EstimateInHours(WorkItem w) {
  return w.Estimate.TotalHours;           // 90 minutes → 1.5
}

TimeSpan Remaining(WorkItem w, TimeSpan spent) {
  return w.Estimate - spent;              // durations subtract, exactly as in C#
}
```

**Arithmetic and comparison** are C#-faithful:

- `dateTime2 - dateTime1` → a `TimeSpan` (how much time elapsed).
- `dateTime + timeSpan` / `dateTime - timeSpan` → a shifted `DateTime`.
- `timeSpan1 + timeSpan2` / `timeSpan1 - timeSpan2` → a `TimeSpan`; `timeSpan * n` scales one.
- `<`, `<=`, `>`, `>=`, `==`, `!=` compare two `TimeSpan`s (and two `DateTime`s).

`TimeSpan` values are computed **in memory** (in function/method bodies). Calling a `TimeSpan` operation — an
arithmetic operator, a factory, or a member — inside a query predicate that lowers to the database is not supported
yet; compute the duration in code.

## Examples       {#examples}
Factories, components, and `DateTime`/`TimeSpan` arithmetic — the window/debounce shape:

```osy title="durations, components, and arithmetic" test app=timespan
double QuotaMinutes() {
  var quota = TimeSpan.FromHours(2);
  return quota.TotalMinutes;                       // 120.0
}

int LeadDays() {
  var span = new TimeSpan(1, 2, 30, 0);            // 1d 2h 30m
  return span.Days;                                // 1
}

TimeSpan Elapsed(DateTime start, DateTime end) {
  return end - start;                              // DateTime − DateTime → TimeSpan
}

bool LongEnough(DateTime start, DateTime end) {
  return (end - start) >= TimeSpan.FromHours(2);   // arithmetic + comparison
}
```

A `TimeSpan` is also a normal entity column:

```osy title="a duration as an entity column" syntax
entity WorkItem {
  string Title;
  TimeSpan Estimate;      // stored as an interval; read back as a TimeSpan
}
```

## See also       {#see-also}
- [Numeric types & literal suffixes](https://osysharp.com/reference/function/numeric-literals/) — the numeric counts the factories take
- [namespace](https://osysharp.com/reference/types/namespace/) — where the built-in value types live


---

<!-- https://osysharp.com/reference/types/duration-and-parts/ -->

# TimeSpan, DateOnly, TimeOnly

> A duration, a bare date, and a bare time of day. Subtracting two DateTimes gives a TimeSpan; adding one back gives a DateTime. All three are exact, and all three work identically in the browser and on the server.

<!-- id: types-duration-and-parts · area: types · stability: stable · html: https://osysharp.com/reference/types/duration-and-parts/ -->

## Summary        {#summary}
`TimeSpan` is a **duration** — an amount of time, with no particular start. `DateOnly` is a **date with no time of
day** (a birthday, a due date). `TimeOnly` is a **time of day with no date** (an opening hour).

## Signature      {#signature}
```osy syntax
due - now                       -> TimeSpan     // subtracting two DateTimes
start + TimeSpan.FromHours(2)   -> DateTime     // adding a duration back

TimeSpan.FromDays(n) · FromHours(n) · FromMinutes(n) · FromSeconds(n) · FromMilliseconds(n)
new TimeSpan(hours, minutes, seconds)  ·  new TimeSpan(days, hours, minutes, seconds)
ts.TotalHours · ts.Hours   (they are not the same — see below)

new DateOnly(2024, 3, 15)  ·  DateOnly.Parse(s)  ·  DateOnly.FromDateTime(d)
new TimeOnly(13, 45)       ·  TimeOnly.Parse(s)  ·  TimeOnly.FromDateTime(d)
```

## Description    {#description}

### Totals and parts are different things   {#totals-vs-parts}

This is the one that trips people up. For a span of one day, two hours and three minutes:

| | Value | What it is |
|---|---|---|
| `ts.Days` | `1` | the **days part** |
| `ts.Hours` | `2` | the **hours part** — never more than 23 |
| `ts.TotalHours` | `26.05` | the **whole span**, expressed in hours |

`Hours` is a component of the written-out duration; `TotalHours` is the duration itself, converted. If you want "how
long was this, in hours", you want `TotalHours`.

### A negative duration is negative all the way down   {#negative}

`TimeSpan.FromHours(-2)` has `Hours` of `-2` — not `22` — and `Minutes` of `0`. Every component carries the sign.

### Durations are exact   {#exact}

A `TimeSpan` holds exact ticks, so accumulating durations never drifts. `TimeSpan.FromSeconds(3.5)` is three and a
half seconds precisely.

### DateOnly and TimeOnly are not DateTimes   {#dateonly-timeonly}

Use `DateOnly` when a time of day would be meaningless — a birthday is a date, not an instant, and giving it a time
invites a timezone to shift it. `DateOnly.FromDateTime(d)` takes the date part of a `DateTime`; `TimeOnly.FromDateTime(d)`
takes the time part.

You can still read the parts off either one directly: `birthday.Year`, `opensAt.Hour`.

## Examples       {#examples}
```osy title="how overdue is it?" test app=durations
string OverdueLabel(DateTime due, DateTime now) {
  var late = now - due;                       // a TimeSpan
  if (late.TotalHours < 24) {
    return $"{late.TotalHours:F1} hours late";
  }
  return $"{late.Days} days late";
}
```

```osy title="a bare date and a bare time" test app=durations
entity Appointment {
  DateOnly Day;
  TimeOnly StartsAt;
}

bool IsMorning(Appointment a) {
  return a.StartsAt.Hour < 12;
}
```

## See also       {#see-also}
- [DateTime](https://osysharp.com/reference/types/datetime/) — the date-and-time type these come from
- [execution side](https://osysharp.com/reference/function/execution-side/) — why all of this runs in the browser too


---

<!-- https://osysharp.com/reference/types/index/ -->

# Types

> The values your app computes with, and the declarations that name and scope them. Most scalar types are exactly C#'s — int, long, double, string, bool, Guid work as you expect. The pages here cover the ones with a wrinkle worth knowing up front (decimal, long, and the date/time family) and the file-level declarations: namespace, visibility, and use.

<!-- id: types-index · area: types · stability: stable · html: https://osysharp.com/reference/types/index/ -->

## Summary        {#summary}
Osy# is C#, so the types are C#'s types. `int`, `long`, `double`, `string`, `bool`, and `Guid` behave exactly as you
expect and need no page of their own. What this area documents is the handful of types with a wrinkle worth knowing
**before** you reach for them — `decimal` and the date/time family — and the three declarations that name and scope
the types you write: `namespace`, visibility, and `use`.

**Looking for the full list?** [Every type, in one list](https://osysharp.com/reference/types/vocabulary/) enumerates every built-in type in one place — the scalars, the
collections, the two callable spellings, and the component-parameter wrappers. Read it when the question is *"is
there a type for this?"* rather than *"how does this one behave?"*.

The one rule that runs through all of it: a value computes to the **same answer on the server and in the browser**,
character for character. A `decimal` total, a `DateTime`'s parts, a `TimeSpan`'s hours — none of them changes with
where the code happens to run.

## Description    {#description}

### The numbers   {#numbers}
Four numeric types, straight from C#: `int` and `long` (whole numbers), `double` (fast, approximate — measurements and
science), and `decimal` (exact base-10 — money, and anything where a fraction of a cent matters). Two earn a page:
[decimal](https://osysharp.com/reference/types/decimal/), because "exact vs approximate" is the choice that quietly decides whether a total is ever a penny
off, and [long](https://osysharp.com/reference/types/long/), because whole-number division truncates and because a 64-bit id is exact to its full range on
both sides. Reach for `decimal` when the number is money, `long` when it is an id or a sequence, `double` when it is a
measurement.

### The date and time family   {#date-and-time}
This is the part to read up front, because the names carry meaning:

- [DateTime](https://osysharp.com/reference/types/datetime/) — a date **and** time. It is a wall-clock value, not an instant on a timeline, so nobody's
  timezone ever shifts it.
- [DateOnly and TimeOnly](https://osysharp.com/reference/types/date-and-time/) — `DateOnly` (a calendar date, no time) and `TimeOnly` (a time of day, no date).
- [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/) — `TimeSpan`, a **duration**: subtract two `DateTime`s and you get one; add it back and you get a
  `DateTime`. Read whole components (`.Days`, `.Hours`) or fractional totals (`.TotalHours`).

[TimeSpan, DateOnly, TimeOnly](https://osysharp.com/reference/types/duration-and-parts/) ties the three together — how they combine, and why all of them are exact and
side-independent.

### The declarations that scope your types   {#scoping}
Three keywords decide where a type lives and who may name it:

- [namespace](https://osysharp.com/reference/types/namespace/) — `namespace X;` at the top of a file puts that file's types in `X`. Optional; omit it and they
  land in the global namespace.
- [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/) — a top-level type is `public` or `internal`, deciding whether code outside its namespace can
  name it. The defaults are C#'s: an entity or enum is `public`, a plain `class` is `internal`.
- [use](https://osysharp.com/reference/types/use/) — `use Osysharp.X;` inside the `app { }` manifest declares a **capability** your app depends on, which
  provisions its tables and types. It is the dependency; a `using` is just the import that brings the names into scope.

### What if my name is already the platform's?   {#shadowing}
The `Osyrin` core namespace is in scope in every app with no `using`, and it exports 82 type names — many of them
ordinary English words (`Slot`, `Group`, `Match`, `Point`, `Month`, `Now`, `Uri`, `Connection`, `Position`). Naming
your own type after one is **legal, and yours wins**; the compiler warns once at the declaration and the platform's
type stays reachable by its full name. [When your name is already the platform's](https://osysharp.com/reference/types/name-shadowing/) has the rule, the full list of taken names, and the
eleven built-in names that are the exception.

### Values the compiler has to know   {#constants}
**Looking for how to DECLARE a named constant? It is `const`, and it goes wherever you need it:**

```syntax
const int EatSoonDays = 90;                       // top level — shared by every function and page in the app
component Home() { const int Rows = 20; … }       // one component
int F() { const int Limit = 5; … }                // one body
class Rules { public const int Retries = 3; }     // on a class, as in C#
```

A `const` folds to its value at every use, so it goes anywhere a literal goes — **including inside a query
predicate**, where a function call cannot (a predicate becomes SQL, and SQL cannot call back into your code). That
is the difference between `const int Days = 90;` and a `int Days() { return 90; }` helper.

For a value that differs between environments — a base URL, a from-address — you want [per-environment config (app.Config)](https://osysharp.com/reference/config/app-config/)
instead, not a constant.

**This section is about something else:** a few places take a value that must be settled while the app is compiled
rather than while it runs — an attribute argument, a config setting, an enum member's label, a workflow message.
[Constant expressions](https://osysharp.com/reference/types/constant-expressions/) covers what counts as constant *there*, including the fact that a long sentence may
be split across lines with `+`.

## See also       {#see-also}
- [Every type, in one list](https://osysharp.com/reference/types/vocabulary/) — EVERY built-in type in one list: scalars, collections, callables, component wrappers
- [decimal](https://osysharp.com/reference/types/decimal/) — exact money arithmetic, and when to prefer it over `double`
- [long](https://osysharp.com/reference/types/long/) — 64-bit whole numbers: ids and sequences, and why division truncates
- [DateTime](https://osysharp.com/reference/types/datetime/) · [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/) · [DateOnly and TimeOnly](https://osysharp.com/reference/types/date-and-time/) — the date/time family
- [namespace](https://osysharp.com/reference/types/namespace/) · [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/) · [use](https://osysharp.com/reference/types/use/) — naming, scoping, and depending
- [When your name is already the platform's](https://osysharp.com/reference/types/name-shadowing/) — the 82 names already in scope, and what happens when you declare one of them yourself
- [string literals — ordinary, verbatim and raw](https://osysharp.com/reference/types/string-literals/) — `"…"`, `@"…"` and `"""…"""`, and the indentation rule that makes a block of prose usable
- [char](https://osysharp.com/reference/types/char/) — one character: `s[0]`, iterating a string, and the `char.*` classification family
- [Constant expressions](https://osysharp.com/reference/types/constant-expressions/) — where a compile-time constant is required, and what folds into one
- [Classes](https://osysharp.com/reference/class/index/) — plain in-memory value shapes; [entity](https://osysharp.com/reference/entity/declaration/) — persisted ones


---

<!-- https://osysharp.com/reference/types/name-shadowing/ -->

# When your name is already the platform's

> The platform puts 82 ordinary English words in scope in every app with no `using` — `Slot`, `Group`, `Match`, `Point`, `Month`, `Now`, `Uri`, `Connection`, `Position`, `Display` … Declaring your own type with one of those names is LEGAL and YOURS WINS everywhere; the compiler NOTES it once, at the declaration, and the platform's type is still reachable by its full name. Nothing is owed and there is nothing to suppress. Eleven names are the exception — the built-in types — and those you cannot take, so those are a warning.

<!-- id: types-name-shadowing · area: types · stability: stable · html: https://osysharp.com/reference/types/name-shadowing/ -->

## Summary        {#summary}
Every app has the `Osyrin` core namespace in scope with **no `using` to write**, and that namespace exports **82
type names**, many of them ordinary English words: `Slot`, `Group`, `Match`, `Point`, `Month`, `Schedule`, `Now`,
`Uri`, `Connection`, `Failure`, `Position`, `Display`, `Cursor`, `Pending`, `Visitor` …

**Declaring your own type with one of those names is legal, and yours wins.** A bare `Slot` anywhere in your app
means *your* `Slot`, in every position — a property type, a local, a parameter, a cast, an enum member access. The
compiler says so once, as a **note at the declaration** (`INFO`, not a warning — nothing is broken and nothing is
owed), and the platform's type stays reachable by its full name (`Osysharp.Workflow.Slot`). Nothing needs renaming: if
`Slot` is the word your domain uses, keep it.

The one exception is the **eleven built-in type names** (`DateTime`, `Json`, `Zone` …) — see [[#built-ins]]. Those
resolve ahead of everything an app declares and have no full spelling, so a type of yours named after one is
unreachable. The compiler warns about those too, and there the remedy is to rename.

## Signature      {#signature}
```osy syntax
enum Slot { Morning, Afternoon }        // legal — and a bare `Slot` now means THIS one, everywhere
Osysharp.Workflow.Slot                    // the platform's, still reachable, by its full name
```

## Description    {#description}

### What happens if I name my type after a platform one?   {#rule}
**Yours wins — the same rule C# uses.** The name you declare in your own app is nearer than a name that arrived from
an import, so it is the one a bare spelling binds to. That holds in *every* position; there is no corner where the
platform's type quietly comes back.

What the compiler owes you is to say it, and it does — once, on the declaration:

```text
model.osy:1  INFO  TYPE_SHADOWS_ALWAYS_IN_SCOPE  `enum Slot` has the same name as 'Osysharp.Workflow.Slot',
             which is in scope in every app with no `using` to write. YOURS WINS: a bare `Slot` anywhere in this
             app means this enum, and 'Osysharp.Workflow.Slot' can only be reached by its full name from here on.
```

That is the whole cost of the collision: a diagnostic that mentions `Slot` may be talking about either type, so you
need to know which one you are reading. There is nothing to accept and nothing to write down — just write the app you
meant to write:

```osy title="the customer's word, kept — and it wins everywhere" test app=types-name-shadowing
enum Slot { Morning, Afternoon, Evening }

entity Booking {
  [Required, MaxLength(120)] string Guest;
  Slot Period;                     // the property type binds to the enum above…
}

Slot PeriodOf(Booking b) {
  Slot pick = Slot.Morning;        // …and so does the local, and the member access
  return b.Period == pick ? pick : b.Period;
}
```

### Can a field be named after its own type?   {#member-named-after-type}
Naming a field after the type it holds is ordinary, and it works:

```osy syntax
component Screen() {
  Valley valley = new Valley();       // the field is named after its own type
  on frame (double dt) {
    valley.Paint();                   // this is the FIELD, not a static call on the type
  }
}
```

A value in scope — a local, a parameter, a class field or a component member — **shadows a type of the same name**,
so `valley.Paint()` is a call on the object you are holding. To reach a `static` member of the type while a value
shadows it, see [[#qualifying]].

### My variable has the same name as my entity — which wins?   {#value-vs-entity}
**Your declaration wins, for as long as it is in scope, and it is decided by its declared TYPE — never by how the
name is spelled.** This is the other axis of the same question, and the same C# answer: a local shadows a type name
in its block, and nobody finds that surprising.

Every binder shadows: a local, a `foreach` variable, a lambda's range variable, a function parameter, a `class`
field read through implicit `this`, and a `component`'s parameters and members read inside an `action` or `method`
body. The entity is untouched everywhere the name is *not* taken — shadowing hides a name, it does not remove one.

```osy title="a field named after an entity is the field; a bare name with nothing in the way is the table" test app=types-name-shadowing
entity Ticket { [Required, MaxLength(80)] string Subject; }

class Envelope {
  public List<string> Ticket;                              // a field named after the entity…
  public int Held() { return Ticket.Take(100).Count; }     // …so this counts the FIELD's strings
}

int InTheStore() { return Ticket.Take(100).Count; }        // nothing shadows it here — the TABLE
```

That matters most for a name you did not choose. A capability you `use` ships components whose parameters were
named without knowing your model — the UI kit alone has `item`, `user`, `entry`, `row` — and you cannot rename
them. They bind to their own declarations, whatever your app calls its entities.

**And the spelling has to match exactly.** Osy# is case-sensitive, as C# is, so `ticket` does not name `Ticket` and
never silently reads it:

```text
model.osy:7  ERROR  RESOLVE_ERROR  unknown identifier 'ticket'. Did you mean 'Ticket'?
```

### Which names are already taken?   {#always-in-scope}
All 82, by the namespace each comes from. Taking one costs you a one-line note and nothing else — this is a list to
recognise a diagnostic by, not a list to avoid.

| Namespace | Names |
|---|---|
| `Osyrin` (51) | `ActionState` · `Align` · `AlignSelf` · `BgSize` · `BorderStyle` · `ConnState` · `Connection` · `Cursor` · `DayOfWeek` · `Display` · `DragAxis` · `Failure` · `FieldSizing` · `FontVariant` · `GridAutoFlow` · `Group` · `GroupCollection` · `Justify` · `MarkdownDocument` · `Match` · `MixBlendMode` · `Month` · `Navigation` · `NavigationRoute` · `Now` · `ObjectFit` · `OutlineStyle` · `Overflow` · `Pending` · `Point` · `PointerEvents` · `Position` · `Resize` · `ScrollBehavior` · `ScrollbarWidth` · `TextAlign` · `TextDecoration` · `TextOverflow` · `TextTransform` · `UiRole` · `UiSort` · `Uri` · `UserSelect` · `Validation` · `VerticalAlign` · `Violation` · `Visibility` · `Visitor` · `WhiteSpace` · `WordBreak` · `Wrapping` |
| `Osysharp.Workflow` (20) | `AuditKind` · `CorrelationOutcome` · `FlowMetrics` · `Leg` · `Losers` · `RequirementStatus` · `Saga` · `ServiceException` · `ServiceHours` · `ServiceWindow` · `SlaKind` · `Slot` · `SlotStatus` · `SlotView` · `StateTime` · `TransitionView` · `WorkflowAuditEntry` · `WorkflowRun` · `WorkflowRunStatus` · `WorkflowRunSummary` |
| `Osysharp.Scheduling` (11) | `Schedule` · `ScheduleExclusion` · `ScheduleFrequency` · `ScheduleOccurrenceOutcome` · `ScheduleOverlap` · `ScheduleRule` · `ScheduleRuleMonth` · `ScheduleRuleMonthDay` · `ScheduleRuleTime` · `ScheduleRuleWeekday` · `ScheduleStatus` |

A capability you `use` brings more names in (the UI kit's controls, for instance). Those follow the same rule and
raise the same note.

### How do I still reach the platform's type?   {#qualifying}
**By its full name**, which is what the note prints. There is nothing else to configure:

```osy title="both types, in one file, each spelled unambiguously" test app=types-name-shadowing
Guid DefinitionOf(Osysharp.Workflow.Slot s) {
  return s.Definition;                      // the PLATFORM's Slot — spelled in full
}

Slot MyDefault() { return Slot.Morning; }   // …and a bare `Slot` is still yours
```

If you ever see a diagnostic like `cannot assign 'Slot' to 'Slot' (type 'Osysharp.Workflow.Slot')`, it now carries a
note saying that the two words are two different types and which one the bare spelling means. That is this rule
speaking at a use site.

### The eleven names you cannot have   {#built-ins}
The **built-in types** are not in a namespace — they are resolved first, ahead of everything an app declares, and
there is no qualified spelling that could reach past them. So a type of yours named after one is **unusable**: the
declaration itself looks fine, and the failure lands later, at a use site.

`DateTime` · `DateOnly` · `TimeOnly` · `TimeSpan` · `Guid` · `Json` · `RichText` · `Markdown` · `Vector` · `Zone` ·
`Culture`

```text
model.osy:1  WARNING  TYPE_SHADOWS_ALWAYS_IN_SCOPE  `enum Zone` has the same name as the BUILT-IN type `Zone` …
             THE BUILT-IN WINS: built-in types resolve ahead of everything an app declares, so a bare `Zone` never
             means this enum — and there is no qualified name that reaches it either, so this enum is unusable.
```

**Rename yours.** `[SuppressWarning]` silences the warning but not the problem — the type stays unreachable. The full
set of built-in types is [Every type, in one list](https://osysharp.com/reference/types/vocabulary/); the ones an entity may store are in [entity members](https://osysharp.com/reference/entity/properties/).

### If you would rather have no collision at all   {#suppress}
You do not have to do anything: the namespace collision is a **note**, so no gate counts it and nothing fails. If you
would rather the two names never met, there are two ways out — rename your type, or put it in a
[namespace](https://osysharp.com/reference/types/namespace/) of its own, which gives it a full name and stops it shadowing anything.

`[SuppressWarning("TYPE_SHADOWS_ALWAYS_IN_SCOPE")]` still works on the declaration if you simply want the line gone
(see [[SuppressWarning]](https://osysharp.com/reference/diagnostics/suppress-warning/)), but it is no longer what the compiler suggests: a note asks for nothing, so
there is nothing to put down. It IS the right tool for the built-in half above, where the warning is real.

## See also       {#see-also}
- [Every type, in one list](https://osysharp.com/reference/types/vocabulary/) — every built-in type in one list
- [namespace](https://osysharp.com/reference/types/namespace/) — `namespace X;`, and why a namespaced type shadows nothing
- [entity members](https://osysharp.com/reference/entity/properties/) — which of these types an entity member may hold
- [[SuppressWarning]](https://osysharp.com/reference/diagnostics/suppress-warning/) — `[SuppressWarning("CODE")]`, and where it may sit


---

<!-- https://osysharp.com/reference/types/char/ -->

# char

> A single character, written in single quotes. It is what you get from `s[0]` and from iterating a string, and it is the argument the `char.*` classification family takes — `char.IsDigit`, `char.IsLetter`, `char.ToUpper`.

<!-- id: types-char · area: types · stability: stable · html: https://osysharp.com/reference/types/char/ -->

## Summary        {#summary}

A single character. Write one in **single quotes** — `'A'`, `'\n'`, `'7'` — the way C# does, and reach for it whenever
you are looking at text one character at a time: scanning a code, validating a format, splitting on a delimiter.

## Signature      {#signature}

```osy syntax
char Initial = 'A';
char Delimiter = ',';
char Tab = '\t';
```

## Description    {#description}

A `char` is one character, not a one-character string, and the distinction earns its keep in two places.

**It is what indexing and iteration give you.** `s[0]` is the first character of `s`; `foreach (var c in name)` walks
them in order. Both read as a `char`, so the classification family below applies directly without a conversion step.

**It picks the overload.** `s.Split(',')` splits on one character; `s.Split(", ")` splits on a two-character
sequence. Those are different operations, they are different overloads in C#, and they stay different here.

### Where it is stored     {#storage}

On an entity, a `char` member is a one-character column. The width is the point: a column that could hold two
characters would let a row exist that the type says cannot.

### Ordering        {#ordering}

Characters compare by code point, so a range test reads the way you would write it in C#:

```osy title="a range test on a character" syntax
c >= 'a' && c <= 'z'
```

### Classification  {#classification}

`char.IsDigit`, `char.IsLetter`, `char.IsLetterOrDigit`, `char.IsWhiteSpace`, `char.IsUpper`, `char.IsLower` and
`char.IsPunctuation` each ask about a single character. `char.ToUpper` and `char.ToLower` hand back a `char`;
`char.Parse` turns a one-character string into one, and **refuses** a string of any other length rather than taking
its first character — `s[0]` is how you say that.

Each of these answers by **Unicode category**, not by an ASCII range. `char.IsDigit('٠')` is true — that is an
Arabic-Indic zero — and `char.IsDigit('²')` is false, because a superscript two is a number but not a digit. The
answer is the same in the browser and on the server.

### One character means one UTF-16 unit    {#code-units}

A `char` holds a single UTF-16 code unit, exactly as in C#. Characters outside the Basic Multilingual Plane — most
emoji, some historic scripts — are **two** units, so they cannot be held in a `char`, and indexing into a string
containing one will land on half of it. When you are handling arbitrary user text rather than a code or a delimiter,
work with the `string` and its own operations instead.

## Examples       {#examples}

```osy title="scanning a string one character at a time" test app=char-basics
int CountDigits(string s) {
  int n = 0;
  foreach (var c in s) {
    if (char.IsDigit(c)) { n = n + 1; }
  }
  return n;
}
```

```osy title="the first character, as a character" test app=char-basics
char Initial(string name) {
  return name[0];
}
```

```osy title="a one-character member on an entity" test app=char-basics
entity Grade {
  [Required, MaxLength(80)] string Subject;
  char Letter;
  security { allow read, create when IsAuthenticated; }
}
```

```osy title="splitting on one character, and on a sequence" test app=char-basics
string FirstField(string row) {
  return row.Split(',')[0];
}
```

### The characters as an array — `ToCharArray`   {#tochararray}
`foreach (var ch in text)` is the usual way to walk a string, and it is what most code wants. (It is the `Text`
module underneath, as every string verb is — you write the fluent form.) When you need the
characters as a value you can index, count or pass on, `text.ToCharArray()` hands them back as a `char[]` — C#'s own
spelling, doing C#'s own thing:

```osy title="when you need them as a value rather than a loop" syntax
char[] letters = code.ToCharArray();
int n = letters.Length;
```

## See also       {#see-also}
- [string literals — ordinary, verbatim and raw](https://osysharp.com/reference/types/string-literals/) — the other quoting form, and the escapes both share
- [Text.Length, Text.IsEmpty, Text.IsBlank, Text.Contains, Text.StartsWith, Text.EndsWith](https://osysharp.com/reference/function/text-inspect/) — asking a string a question: contains, starts with, index of


---

<!-- https://osysharp.com/reference/types/decimal/ -->

# decimal

> Exact base-10 arithmetic, for money and anything else where a fraction of a cent matters. It behaves identically whether your code runs on the server or in the browser.

<!-- id: types-decimal · area: types · stability: stable · html: https://osysharp.com/reference/types/decimal/ -->

## Summary        {#summary}

Exact base-10 arithmetic, for money and anything else where a fraction of a cent matters. `decimal` is not a binary
float: `0.1 + 0.2` is exactly `0.3`, and it behaves identically whether your code runs on the server or in the browser.

## Signature      {#signature}

```osy syntax
decimal Total = 19.99m;      // the `m` suffix makes a literal a decimal
```

## Description    {#description}

Use `decimal` for money, quantities, tax rates, percentages — anything where the answer has to be *the* answer rather
than a very close one. Use `double` for measurements and scientific values, where the extra range matters more than the
last digit.

The difference is not cosmetic. A binary float cannot represent `0.1` exactly, so `0.1 + 0.2` lands a hair above `0.3`
and `1.005 * 100` lands a hair *below* `100.50` — which then rounds to `100.49`. A `decimal` stores the digits you
wrote, so it gives the answer you would give.

```osy title="why a double gets money wrong" syntax
decimal price = 1.005m;
decimal total = price * 100m;      // exactly 100.500 — a double would say 100.49999999999999
```

### The same answer everywhere   {#same-everywhere}

An Osy# expression means one thing. A `decimal` is exact **wherever the function runs** — server-side, or in-process in
the browser (see [execution side](https://osysharp.com/reference/function/execution-side/)). A comparison that is true on one side is true on the other; a total that
prints `"1.10"` on one side prints `"1.10"` on the other. You do not need to know, or care, where a piece of code
executes in order to trust its arithmetic.

### Rounding is *banker's* rounding   {#rounding}

`Math.Round` rounds **half to even**, not half up. That is deliberate: always rounding `.5` upward biases a long column
of figures steadily upward, which is exactly what you do not want in a ledger.

```osy title="Math.Round goes half to EVEN, not half up" syntax
Math.Round(2.5m)    // 2   — not 3
Math.Round(3.5m)    // 4
Math.Round(1.005m, 2)   // 1.00
```

Round explicitly when you present a value. Rounding *as you go* accumulates error just as surely as a float would.

### Trailing zeros are part of the value   {#trailing-zeros}

`1.10m` keeps its two decimal places and prints as `"1.10"` — a currency total keeps its cents column. But equality is
numeric, so `1.1m == 1.10m` is **true**. Arithmetic follows the same rules you would use on paper: addition takes the
wider of the two scales, and multiplication adds them.

```osy title="trailing zeros survive, but equality is numeric" syntax
1.10m + 2.20m       // 3.30
1.5m * 1.5m         // 2.25
1.1m == 1.10m       // true
```

## Examples       {#examples}

```osy title="a line total is exact" test app=decimal-basics
class Line {
  decimal Price;
  int Quantity;

  decimal Total() {
    return Price * Quantity;
  }
}
```

```osy title="round once, at the end" test app=decimal-basics
decimal WithTax(decimal subtotal, decimal rate) {
  return Math.Round(subtotal * (1m + rate), 2);
}
```

## See also       {#see-also}
- [execution side](https://osysharp.com/reference/function/execution-side/) — where a function runs; a decimal is exact on either side


---

<!-- https://osysharp.com/reference/types/long/ -->

# long

> A 64-bit whole number, exact to its full range — ids, sequence numbers, row versions, byte offsets. It holds the same value whether your code runs on the server or in the browser, including past the point a floating-point number would start rounding.

<!-- id: types-long · area: types · stability: stable · html: https://osysharp.com/reference/types/long/ -->

## Summary        {#summary}

A 64-bit whole number, exact across its entire range — roughly ±9.2 quintillion. Use it for values that are counted or
issued rather than measured: ids, sequence numbers, row versions, byte offsets. Like every other value in Osy#, a
`long` holds the same value wherever the code runs.

## Signature      {#signature}

```osy syntax
long Sequence = 9007199254740993;
int  Count    = 42;                  // `int` is 32-bit — plenty for a count
```

## Description    {#description}

Reach for `long` when a number is an **identity or a position**, not a measurement. An id issued by a sequence, a
version stamp, an offset into a file — these are values where being off by one is not a small error, it is the wrong
record. `int` covers ordinary counts and indexes; use `long` when the range could plausibly exceed about two billion,
or when the value comes from a system that issues 64-bit ids.

### Division truncates   {#division}

Whole-number division discards the remainder rather than rounding — `7 / 2` is `3`, and `-7 / 2` is `-3`. The remainder
operator takes the sign of the left operand, so `-7 % 2` is `-1`.

```osy title="whole-number division discards the remainder" syntax
7 / 2          // 3    — not 3.5, and not 4
-7 / 2         // -3   — truncated toward zero
-7 % 2         // -1   — the sign follows the dividend
```

This holds **anywhere in an expression**, not only when the two operands are written side by side. A whole-number
expression stays a whole-number expression however many steps it takes to get there, so a division at the end of a
chain truncates exactly as a direct one does.

```osy title="a chain of whole numbers still truncates at the end" syntax
(3 - 1) / 3    // 0    — the subtraction is still whole-number arithmetic
(9 - 1) / 3    // 2
count * 2 / 3  // truncates; every operand is a whole number
```

If you want the fractional answer, ask for one: use `decimal` (see [decimal](https://osysharp.com/reference/types/decimal/)) or `double` for the operand.
One fractional operand makes the **whole** expression fractional, wherever it appears in the chain.

```osy title="one fractional operand wins the whole expression" syntax
7m / 2m        // 3.5
(3 - 1) / 3m   // 0.666… — the decimal operand wins the expression
```

### Arithmetic fails at the edges rather than wrapping   {#overflow}

A `long` has a fixed width, so there are values arithmetic on it cannot produce. Running past the maximum **raises**,
naming the operands and the range — it does not wrap around to the minimum, and it does not quietly grow into a wider
type. Negating the minimum value raises for the same reason: its positive counterpart does not exist in 64 bits.

This is a deliberate difference from C#, which wraps by default. A wrapped total is not an obviously broken value like
a blank or an error — it is a plausible number of the wrong sign, and nothing downstream can tell it from a right one.
The same expression pushed down into the database raises too, so you get one answer wherever it runs.

If a value can legitimately grow past a `long`, say so in the type: use `decimal` (see [decimal](https://osysharp.com/reference/types/decimal/)) or `double`.

### The same answer everywhere   {#same-everywhere}

An Osy# expression means one thing. A `long` is exact **wherever the function runs** — server-side, or in-process in
the browser (see [execution side](https://osysharp.com/reference/function/execution-side/)) — including for values above 2^53, where a floating-point number would
silently round to a nearby value. Two ids that differ only in their last digit stay two different ids on both sides,
and a comparison that is true on one side is true on the other.

This matters more than it sounds. A rounded id is not an obviously broken value like a blank or an error; it is a
perfectly plausible id belonging to a different row. You do not need to know, or care, where a piece of code executes
in order to trust that the id you are holding is the one you were given.

## Examples       {#examples}

```osy title="an externally-issued id keeps every digit" test app=long-basics
entity Event {
  long ExternalId;
  [MaxLength(100)] string Name;
}
```

```osy title="paging by offset, in whole numbers" test app=long-basics
long PageOffset(long pageIndex, long pageSize) {
  return pageIndex * pageSize;
}
```

```osy title="ask for a fractional answer explicitly" test app=long-basics
decimal AveragePerItem(decimal total, long items) {
  return items == 0 ? 0m : total / items;
}
```

## See also       {#see-also}
- [decimal](https://osysharp.com/reference/types/decimal/) — exact base-10 arithmetic, for money and anything with a fractional part
- [execution side](https://osysharp.com/reference/function/execution-side/) — where a function runs; a `long` is exact on either side


---

<!-- https://osysharp.com/reference/types/namespace/ -->

# namespace

> Declares the namespace a file's types belong to, written once at the top of the file. It is optional — a file without one puts its types in the global namespace. Names resolve against the namespace a file is written in, then any imported namespaces, then the always-available `Osyrin` core.

<!-- id: types-namespace · area: types · stability: stable · html: https://osysharp.com/reference/types/namespace/ -->

## Summary        {#summary}
A **namespace** groups a file's types under a common name, so two parts of an app can each have an `Order`
without colliding. Write it once, at the top of the file, terminated by a semicolon:

```osy syntax
namespace Shop.Catalog;
```

It is **optional**. A file with no `namespace` declaration puts its types in the **global namespace** — which is
exactly what a single-file app wants, and what every example in this reference assumes.

## Signature      {#signature}
```osy syntax
namespace Shop;              // this file's types are Shop.*
entity Order { … }           // → Shop.Order
```

## Description    {#description}

### What it does to a name   {#qualified-name}
A type declared in a namespace gets that namespace as a prefix. `namespace Shop;` followed by `entity Order`
declares **`Shop.Order`** — that is the type's real, full name, the one you use to refer to it from elsewhere.

Inside the file that declares it, you just write `Order`.

### Two forms — file-scoped and braced   {#forms}
Osy# supports both C# forms:

```osy title="file-scoped or braced — one form per file" syntax
namespace Shop;                          // file-scoped — opens the whole file (preferred)

namespace Shop { entity Order { … } }    // braced — opens just its block
namespace Billing { entity Order { … } } // several braced namespaces may share one file
```

- **File-scoped** (`namespace X;`) opens the whole file: it must come **before every other declaration**, and a
  file may declare **at most one**. Prefer it for a file that is all one namespace.
- **Braced** (`namespace X { … }`) opens just its block, so a file may hold **several** and they may **nest**
  (`namespace Shop { namespace Catalog { … } }` puts a type in `Shop.Catalog`). This is what lets C# code paste in
  verbatim.

A file uses **one form or the other, never both** — mixing a file-scoped `namespace X;` with a braced
`namespace Y { … }` in the same file is an error (as in C#).

### How a name resolves   {#resolution}
When you write a bare name, it is looked for in this order — the first match wins, and no later step can make it
ambiguous:

1. **The namespace you're in**, then each enclosing one, working outward. Inside `namespace Shop.Catalog;` that
   means `Shop.Catalog`, then `Shop`, then the global namespace.
2. **The namespaces you imported** with a `using` declaration. If *two* imports offer the same name, that is
   an error — qualify the reference to say which you mean.
3. **The `Osyrin` core**, the platform's own namespace, always available without importing anything.

Because step 1 comes first, a type you declared always wins over one you imported. To reach the imported one
anyway, write its full name.

### The `Osyrin` namespace is the platform's   {#osyrin}
Everything the platform ships lives under `Osyrin`. Its core types need no import. Its optional capabilities are
sub-namespaces — `Osysharp.Memory`, `Osysharp.Storage`, `Osysharp.Ui`, … — that you first **depend on** with a
[`use`](https://osysharp.com/reference/types/use/) in your `app { }` manifest, then **import** with a `using` in each file that references their
names. You cannot declare a type in `Osyrin` yourself.

### Reaching another namespace   {#qualifying}
A sibling namespace's types are not visible bare — qualify them, exactly as in C#:

```osy title="reaching a type in a sibling namespace" syntax
namespace Billing;

int Count() {
  var orders = Shop.Order.Where(o => o.Code == "x").ToList();   // qualified
  return orders.Count;
}
```

### Declaration names never contain a dot   {#no-dots}
`entity Shop.Order` is an error. A dot in a declared name is how you'd *spell* a namespace, not how you *declare*
one — use `namespace Shop;` at the top of the file.

## Examples       {#examples}
```osy title="a file written in a namespace" test app=types-namespace
// catalog.osy
namespace Shop;

entity Order {
  [Required] string Code;
  decimal Total;
}

int OpenOrders() {
  // `Order` resolves to Shop.Order — the namespace this file is written in.
  return Order.Where(o => o.Total > 0).ToList().Count;
}
```

```osy title="reaching another namespace's type" test app=types-namespace
// billing.osy — a different namespace, so Shop.Order must be qualified.
namespace Billing;

entity Invoice {
  Shop.Order Source;
  decimal Amount;
}
```

Step 3 applies in every position a type name can appear, including a property's type — a core type needs no import
there either:

```osy title="a core type by its bare name" test app=types-namespace
namespace Filing;

entity Attachment {
  // `MarkdownDocument` is `Osysharp.MarkdownDocument`, found at step 3. Nothing is imported and nothing is qualified.
  MarkdownDocument Body;
}
```

## See also       {#see-also}
- [use](https://osysharp.com/reference/types/use/) — `use` declares the capability dependency in the manifest; `using` imports its names in a file.
- [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/) — `public` and `internal` decide which of a namespace's types others can reach.
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — `using` brings a kit's public types into scope under their bare names.


---

<!-- https://osysharp.com/reference/types/string-literals/ -->

# string literals — ordinary, verbatim and raw

> Three ways to write a string, all of them C#'s. The ordinary form processes escapes. The verbatim form (`@"…"`) processes none and may span lines, which is what regular expressions and Windows paths want. The raw form (`"""…"""`) processes none either, lets you write quotes plainly, and — for the multi-line shape — strips the closing delimiter's own indentation, so a block of prose lines up with the code around it without any of that alignment reaching the value.

<!-- id: types-string-literals · area: types · stability: stable · html: https://osysharp.com/reference/types/string-literals/ -->

## Summary        {#summary}

```osy syntax
var a = "line one\nline two";           // escapes processed
var b = @"C:\temp\report.csv";          // no escapes; "" is a literal quote; may span lines
var c = """He said "yes" and left.""";  // no escapes; quotes written plainly
```

Pick by what the text contains. Escapes are convenient until the text is full of backslashes; then `@"…"` is
clearer. Both get awkward once the text is a *paragraph* — which is what the raw form is for.

## Signature      {#signature}

| form | escapes | quotes inside | spans lines |
|---|---|---|---|
| `"…"` | processed (`\n`, `\t`, `\\`, `\"`, `\uXXXX`) | `\"` | no |
| `@"…"` | none | `""` | yes, verbatim — every leading space is kept |
| `"""…"""` | none | written plainly | yes, and the closing delimiter's indentation is stripped |

## Description    {#description}

### The raw form, single line   {#raw-single}
Everything between the delimiters, exactly:

```osy title="quotes inside, nothing escaped" syntax
var q = """He said "yes" and left.""";      // He said "yes" and left.
```

Open with more than three quotes when the text itself contains three:

```osy title="a longer fence when the text holds three quotes" syntax
var fence = """"a ``` and a """ inside"""";
```

The rule is that the closing delimiter is at least as long as the opening one, so the author picks a fence longer
than anything inside. Same as C#.

### The raw form, multi-line — and the indentation rule   {#raw-multi}
Put nothing but whitespace after the opening delimiter and the literal becomes multi-line. Then:

- the first newline and the last newline are **not** part of the value;
- **the closing delimiter's indentation is stripped from every line**.

That second rule is the whole reason the form exists. It lets a block of prose sit at the indentation of the code
around it while none of that indentation reaches the value:

```osy syntax
agent Auditor {
  Prompt = """
    You review expense claims.

    Meals are reimbursable up to 60 per person per day.
    """;
}
```

The value is `You review expense claims.\n\nMeals are reimbursable up to 60 per person per day.` — no leading spaces,
no blank first line, no trailing newline. Written as `@"…"` the same block would carry four spaces on every line
into the value, and written as concatenated `"…"` fragments it would not be readable as prose at all.

⚠ **A line indented LESS than the closing delimiter is a compile error**, not a partial strip. The alternative is a
value whose leading whitespace depends on where in the file it was written, which nothing downstream could report.
Line the text up with the closing delimiter, or move the delimiter left.

⚑ **A blank line is exempt.** It has no indentation to disagree with, and requiring some would mean trailing spaces
on every empty line of a paragraph.

### Why raw literals matter most for a prompt   {#why}
An [agent](https://osysharp.com/reference/agent/loop/)'s `Prompt` is the clearest case: it is prose, it is inherently multi-line, and it is the most
important text on the declaration. The instructions a model actually receives should be readable in the source that
supplies them.

## Examples       {#examples}

A multi-line prompt, and a verbatim path, in one app:

```osy title="raw-and-verbatim-strings" test app=string-literals
entity Note {
  [Required, MaxLength(4000)] string Body;
  security { allow read, create, update when IsAuthenticated; }
}

/// The multi-line raw form: indented with the code, and none of that indentation is in the value.
string Guidance() {
  return """
    Keep a note short.

    A note that needs headings is a document, and belongs somewhere else.
    """;
}

/// The single-line raw form — quotes written plainly, no escaping.
string Quoted() {
  return """She said "no" twice.""";
}

/// Verbatim: no escapes, so a backslash is a backslash.
string ExportPath() {
  return @"C:\exports\notes.csv";
}
```

## See also       {#see-also}
- [Every type, in one list](https://osysharp.com/reference/types/vocabulary/) — every built-in type in one list
- [Constant expressions](https://osysharp.com/reference/types/constant-expressions/) — where a compile-time constant is required
- [the agent loop (app.Agent, Loop)](https://osysharp.com/reference/agent/loop/) — the multi-line prompt this form was built for


---

<!-- https://osysharp.com/reference/types/visibility/ -->

# type visibility (public / internal)

> A top-level type carries a public or internal visibility that decides whether code outside its namespace can name it. The default depends on what you declared: an entity or enum is public, a class is internal (exactly C#). A component has no visibility modifier at all — its access is governed by authorization.

<!-- id: types-visibility · area: types · stability: stable · html: https://osysharp.com/reference/types/visibility/ -->

## Summary        {#summary}
Every top-level type has a **visibility** that decides whether code outside its namespace can name it. A
**`public`** type is part of the surface its namespace offers outward; an **`internal`** type is an
implementation detail, usable freely inside its own namespace and invisible beyond it.

You rarely have to write the modifier, because the **default follows what you declared**: an `entity` or an
`enum` is public, a `class` is internal. A `component` has no visibility modifier at all.

## Signature      {#signature}
```osy syntax
entity Product { string Name; }            // public — an entity is a data surface
enum Status { Draft, Active }              // public — an enum is vocabulary
class Money { public decimal Amount; }     // internal — exactly C#

internal entity Ledger { … }               // an entity kept inside its namespace
public class Money { … }                   // a class offered outward
component Badge(string label) { … }        // no modifier — see below
```

## Description    {#description}

### The defaults, and why they differ   {#defaults}
| You declare | Default | Why |
|---|---|---|
| `entity` | `public` | An entity **is** the app's data surface — its fields are already public, so the type is too. |
| `enum` | `public` | An enum is vocabulary: it appears as the field type of a public entity, so an internal default would create friction at every use. |
| `class` | `internal` | Exactly C#. A class stays **source-portable** — it must lift into an external C# project verbatim, so its rules follow C#'s without deviation. |
| `component` | *(none)* | A component's access is governed by **authorization**, not visibility. See below. |

An explicit `public` or `internal` always overrides the default. `private` is not a top-level modifier — C# has
no private top-level types, and neither does Osy#.

### What visibility controls   {#controls}
Visibility is a **hard access rule**, not a decoration. An `internal` type cannot be named from outside its
namespace, and `using SomeNamespace;` brings only that namespace's **public** types into scope. This is how a
published unit distinguishes what it offers from what it merely uses.

Because an `entity` and an `enum` default to **public**, a unit that means to keep one to itself must say so:

```osy syntax
entity Product { … }             // exposed to anyone who imports this namespace
internal entity PriceHistory { … } // an implementation detail — say `internal` explicitly
```

### A component has no visibility   {#components}
Writing `public component` or `internal component` is an **error**. A component's reachability is decided on a
different axis — **authorization**: a routed component requires an authenticated caller unless it is marked
`[AllowAnonymous]`, and `[Composable]` governs whether it may be rendered inside another component's surface.
Type visibility would be a second, redundant gate answering a question authorization already answers, so a
component simply doesn't have one.

### Entity fields are always public   {#entity-fields}
A visibility modifier on an **entity field** is an error — the field *is* the data, so it is always public. A
`class` member is different: it carries its own [`public`/`private`](https://osysharp.com/reference/class/constructors/) **member**
visibility, and a bare member is `private`, exactly as in C#.

## Examples       {#examples}
```osy title="what each default is, and how to override it" test app=types-visibility
/// A catalog item other code may reference. `entity` is public by default.
entity Product {
  [Required] string Name;
  decimal Price;
}

/// An internal roll-up used only inside this namespace — say `internal` to keep it in.
internal entity PriceHistory {
  Product Item;
  decimal Was;
}

/// A class is internal by default (C#); mark it `public` to offer it outward.
public class Money {
  public decimal Amount;
  public string Currency;
}
```

## See also       {#see-also}
- [namespace](https://osysharp.com/reference/types/namespace/) — the namespace a type lives in, and what `internal` therefore keeps it inside of.
- [constructor](https://osysharp.com/reference/class/constructors/) — member visibility (`public`/`private`) on class members, the other visibility axis.
- [component](https://osysharp.com/reference/ui/component/) — why a component's access is an authorization question, not a visibility one.


---

<!-- https://osysharp.com/reference/types/use/ -->

# use

> Declares a capability your app depends on, written inside the `app { }` manifest block. It provisions the capability (its tables and types become available) and, for a kit, pins a version. It is the dependency; a `using` in a source file then imports the capability's names.

<!-- id: types-use · area: types · stability: preview · html: https://osysharp.com/reference/types/use/ -->

## Summary        {#summary}
**`use`** declares a **capability your app depends on**. It goes inside the `app { }` manifest block — the one
place a project says what it is built from:

```osy syntax
app Shop {
  model "model/**/*.osy";
  use Osysharp.Memory;      // depend on the Memory capability
  use Osysharp.Ui@2;        // depend on the UI kit, pinned to major 2
}
```

A `use` is your app's **dependency** — the equivalent of a package reference. It provisions the capability (its
tables and types become part of your app) and, for a versioned kit, records which version you want. To actually
**reference** a capability's names in a file, add a [`using`](#using-vs-use) for it there.

## Signature      {#signature}
```osy syntax
use Osysharp.Memory;        // a platform capability — version-neutral
use Osysharp.Ui@2;          // a kit — pinned to a major (@2 = any 2.x; @2.3 = ≥ 2.3 within 2.x; @2.4.1 = exact)
```

`use` is only valid **inside the `app { }` block**. A version pin (`@…`) is only meaningful on a **kit** — pinning
a version-neutral platform capability (which rides the platform binary) is an error.

## Description    {#description}

### What a dependency does   {#dependency}
Declaring `use Osysharp.Memory;` makes the Memory capability part of your app: its entities and types are
provisioned, its features (here, semantic `[Searchable]` fields and `Memory.Search`) become available. Remove the
`use` and the capability — and everything that needs it — is gone. The manifest is the single, authoritative list
of what your app depends on.

### `using` vs `use`     {#using-vs-use}
They are two different things, exactly as a C# project separates its **package references** from its **imports**:

| | Where | What it does |
|---|---|---|
| **`use Osysharp.Memory;`** | inside `app { }` | the **dependency** — provisions the capability, pins a kit version |
| **`using Osysharp.Memory;`** | at the top of any source file | the **import** — brings the capability's names into that file's scope |

A file that references a capability's names imports them with `using`, just like reaching any other namespace:

```osy syntax
// model/note.osy
using Osysharp.Memory;

entity Note {
  [Searchable] string Body;      // the [Searchable] feature comes from the Memory capability
}
```

If your app declares an `app { }` manifest, that manifest is **authoritative for CAPABILITIES**: a
`using Osysharp.X;` for a capability you did not `use` is an error — the same way C# rejects a `using` for an assembly
you never referenced. Add the matching `use` to the manifest to fix it. (A quick throwaway snippet with no manifest
at all is unconstrained — there, a `using` provisions on its own.)

### A bundled KIT needs no `use` — the `using` is enough, and `Osysharp.Ui` needs neither   {#kits-self-provision}
`Osysharp.Ui`, `Osysharp.Markdown` and `Osysharp.Charts` **ship inside the platform**. There is nothing to fetch and
nothing to choose, so for `Osysharp.Markdown` and `Osysharp.Charts` the `using` is the whole declaration — the kit
composes for the files that import it, and an app that never imports it pays nothing.

**`Osysharp.Ui` goes one step further: it is in scope for EVERY app, with nothing written at all.** No `using`, no
`use`. It is the kit almost every app reaches for, and `using` is FILE-scoped — so the opt-in was not one line per
app but one line per file, and the line you forget is in the file you wrote last. Both spellings stay legal; an
explicit `using Osysharp.Ui;` is simply redundant.

**A `use` for a kit is a version PIN, not a permission.** Write it when you want one:

```osy syntax
app Shop {
  model "model/**/*.osy";
  use Osysharp.Ui@2;        // pin the kit — not needed just to USE it
  use Osysharp.Memory;      // a CAPABILITY: still declared, because it stands up a vector store
}
```

The line between them is what the dependency DOES. A kit is syntax you import; a capability changes the shape of
your app — `Osysharp.Memory` a vector store, `Osysharp.Storage` a blob store, `Osysharp.Observability` audit tables — and
the manifest is where an app's shape is declared.

### Version pins live on `use`   {#versions}
A version belongs on the dependency, never on the import. `use Osysharp.Ui@2;` pins the kit; a `@version` written
on a `using` is an error that points you back to the `use`.

## Examples       {#examples}
```osy title="the manifest declares the dependency" test app=use-manifest-declares
// app.osy — the manifest declares the app's dependencies with `use`.
app Shop {
  model "model/**/*.osy";
  use Osysharp.Ui@2;
  use Osysharp.Memory;
}
```

```osy title="each file imports what it references" test app=use-file-imports
// model/catalog.osy — a file imports what it references with `using`.
using Osysharp.Ui;
using Osysharp.Memory;

entity Product {
  [Required] string Name;
  [Searchable] string Description;
}
```

## See also       {#see-also}
- [namespace](https://osysharp.com/reference/types/namespace/) — how a bare name resolves through the namespaces you import and the `Osyrin` core.
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — the UI kit you depend on with `use Osysharp.Ui;` and import with `using Osysharp.Ui;`.
- [Pinning a kit version (using Ui@2)](https://osysharp.com/reference/ui/kit-versioning/) — pinning a kit's major version on the `use`.


---

<!-- https://osysharp.com/reference/ui/query-failure/ -->

# A failing query

> When a query a region reads fails or is refused, that region shows the failure in place — the server's own sentence, the correlation id to look it up by, and a Retry — instead of rendering as empty. It is automatic: you write nothing. The rest of the page keeps working, because only the data is missing.

<!-- id: ui-query-failure · area: ui · stability: stable · html: https://osysharp.com/reference/ui/query-failure/ -->

## Summary        {#summary}
When a region reads a query that **fails** — the server refused it, the request errored, the connection dropped — that
region renders the failure **where the rows would have been**. You write nothing; any `foreach` over a query member gets
it.

It exists because the alternative is silence. A failed query has no rows, and a region with no rows renders as *empty* —
so a table the user is not allowed to see looked exactly like a table with nothing in it. That is not a small confusion:
it is the difference between "there are no expenses" and "you were refused", and the user cannot tell which they are
looking at.

The failure is **in-page**, not a whole-page error screen. The page rendered fine and its other controls still work, so
replacing all of it would misdescribe what broke: only the data is missing. A page whose other half is a form the user
was half-way through filling in must not lose that form because a list beside it failed to load.

For a page that fails to **load at all** — no data, no render — the app's error surface is the right thing; that is a
different situation with a different answer. For the *waiting* state before an answer arrives, see [Pending](https://osysharp.com/reference/ui/pending/).

## Signature      {#signature}
```osy syntax
// Automatic — no code. A `foreach` over a query whose fetch failed renders the failure in place of its rows.
```

The rendered affordance, for styling:

| Class | What it is |
|---|---|
| `.osy-query-failed` | the container (carries `role="alert"`) |
| `.osy-query-failed-message` | the server's sentence, or the transport error when there is none |
| `.osy-query-failed-correlation` | the correlation id — present only when the failure carried one |
| `.osy-query-failed-retry` | the Retry button |

## Examples       {#examples}
Nothing here opts in — the `foreach` is ordinary, and the affordance appears only if the read is refused or errors:

```osy title="a list that reports its own refusal" test app=ui-query-failure
[Principal] entity User { [Required] string Email; }

entity ExpenseLine {
  [Required] string Description;
  decimal Amount;
  security { allow read where CreatedBy == user.Id; }   // a refusal here is what the affordance reports
}

[Page("/expenses")] [Render(CSR)]
component Expenses() {
  live var lines = ExpenseLine.OrderBy(l => l.Description).ToList();
  render {
    Stack(gap: 2) {
      // No failure handling written here: if this query is refused, the failure renders in place of the rows.
      foreach (var l in lines) { Text(l.Description); }
    }
  }
}
```

## Description    {#description}
**What it says.** The message prefers the **server's own sentence** over the transport's. A refusal that says *"You do
not have access to these lines."* is worth showing; `GET /query/… failed: 403` tells the user nothing they can act on.
When the failure carries no sentence of its own — a dropped connection, a DNS failure — the transport's message is shown
instead, because something specific always beats a blank box.

**The correlation id** is the id to run `osy logs --correlation <id>` with. It appears when the failure carried one, so
whoever hit the problem can report *which* failure they hit. A failure a user cannot report is most of the way to a
failure nobody can fix.

**Retry** re-runs that query and nothing else. On success the affordance disappears and the rows render through the
ordinary path; on a second failure it stays, with whatever the server said this time. Retrying does not reload the page
or re-run the page's other queries.

**It is entirely client-side, and it is mechanism.** The platform draws a plain container with `osy-` classes — the same
arrangement as the built-in `.osy-spinner` — and an app restyles it in its own CSS alongside every other control state.

**Scope.** It covers a region reading a query member: a `foreach` over one, including an inline query. A query that
succeeds renders no affordance at all.

## Examples       {#examples}
The common case is **nothing** — this is automatic:
```osy title="only the failed region reports it — the rest keeps working" syntax
component ReportDetail(ExpenseReport report) {
  query lines = ExpenseLine.Where(l => l.Report == report);
  render {
    Text(report.Title);              // still renders if `lines` fails
    foreach (var l in lines) {       // if the query is refused, THIS region shows why, with a Retry
      Text(l.Merchant);
    }
    Button("Save", onPress: Save);   // still works
  }
}
```

Restyling it to match your app:
```css
.osy-query-failed {
  display: flex; align-items: center; gap: .75rem;
  padding: .75rem 1rem; border: 1px solid var(--danger-border); border-radius: 6px;
}
.osy-query-failed-correlation { font: 12px/1 monospace; opacity: .6; }
```

## See also       {#see-also}
- [Pending](https://osysharp.com/reference/ui/pending/) — the *waiting* state: the automatic per-control spinner, `save.Pending`, and the page-wide `Pending`
  ambient.
- [Connection](https://osysharp.com/reference/ui/connection/) — the sibling case where the server itself became unreachable, which is a whole-page condition.
- [public pages (what a signed-out visitor can see and do)](https://osysharp.com/reference/security/public-reads/) — what decides whether a query is refused in the first place.


---

<!-- https://osysharp.com/reference/ui/reordering/ -->

# An order the person maintains

> When the order of a list is a fact the person owns rather than something a field implies, store it: an int Position on the row, written when the row is added and swapped with its neighbour to move it. Sorting by name, by price or by created-at renders a list that looks right and is not the one that was asked for.

<!-- id: ui-reordering · area: ui · stability: stable · html: https://osysharp.com/reference/ui/reordering/ -->

## Summary        {#summary}
Sometimes the order of a list is **a fact the person owns**. A rehearsal running order, the steps of a recipe, a
reading pile, the leg order of a relay — none of these is implied by any field the record already has.
It is not alphabetical, it is not the order they were entered in, and it is not derivable from a price or a date.

When that is true, the order is **data**, and it has to be stored:

```osy title="the position is a field, because nothing else implies it" test app=ui-reordering
entity Movement {
  [Required, MaxLength(120)] string Name;
  [Min(0)] int Minutes = 0;
  bool Played = false;
  // ⚑ THE DECISION. The running order is the conductor's, so it is stored. Sparse on purpose (10, 20, 30…):
  // moving one then swaps two numbers instead of renumbering the whole programme.
  [Min(0)] int Position;
  security { allow read, create, update, delete when IsAnonymous || IsAuthenticated; }
}
```

⚠ **Why it has to be stored.** An app that keeps no position can sort its list beautifully and still not be
showing the sequence anyone chose: `OrderBy(m => m.Name)` renders, every test against that page passes, and the sequence the
person asked for cannot be expressed at all. Ask whether the order is derivable from the row. If it is not, store it.

## Signature      {#signature}
```osy syntax
[Min(0)] int Position;              // on the entity

Entity.OrderBy(x => x.Position).ToList()      // to read it
```

## Description    {#description}

### Adding at the end          {#adding-at-the-end}
A new row goes after everything already there, so the new position is the highest so far plus a step.

> ⚠ **`FirstOrDefault()` answers differently over a QUERY and over a LIST, and the two look identical in source.**
> Over rows read from the store it answers **ABSENT** — an `int?` — because "no row matched" and "a row matched
> and its value is 0" are different facts and a query can tell them apart. Over an in-memory list it answers
> **`0`**, exactly as C# does, because by then you are holding the list and the distinction is gone.
>
> ```osy syntax
> int? highest = Movement.Select(m => m.Position).FirstOrDefault();       // ABSENT when the table is empty
> var  loaded  = Movement.ToList();
> int  fallback = loaded.Select(m => m.Position).FirstOrDefault();        // 0 when the list is empty — C#'s answer
> ```
>
> So `?? 0` is **required** on the first and **redundant** on the second. You do not have to remember which: the
> compiler refuses `int x = <the query form>` and names the fix (*"declare it `int? x` if absent is a case you
> handle"*). The list form simply compiles, because it is already an `int`.

```osy title="the highest so far, or none yet" test app=ui-reordering
[AllowAnonymous]
void AddMovement(string name, int minutes) {
  // `FirstOrDefault()` over a number answers ABSENT when nothing matched — not C#'s 0 — so give absence a value.
  // `First()` would THROW on the empty programme, which is the very first movement anybody adds.
  var last = Movement.OrderByDescending(m => m.Position).Select(m => m.Position).FirstOrDefault() ?? 0;
  new Movement { Name = name, Minutes = minutes, Position = last + 10 };
  UnitOfWork.Commit();
}
```

### Moving one up or down      {#moving}
"Move it up" means **swap with the neighbour** — the row immediately above it in the stored order. Nothing
renumbers, so moving a row is two writes however long the list is.

```osy title="swap with the neighbour above; the one below is the mirror image" test app=ui-reordering
[AllowAnonymous]
void MoveUp(Movement m) {
  var above = Movement.Where(x => x.Position < m.Position)
                      .OrderByDescending(x => x.Position).FirstOrDefault();
  if (above == null) { return; }        // already first — see the note about the ARROW, below
  var p = above.Position;
  above.Position = m.Position;
  m.Position = p;
  UnitOfWork.Commit();
}

[AllowAnonymous]
void MoveDown(Movement m) {
  var below = Movement.Where(x => x.Position > m.Position)
                      .OrderBy(x => x.Position).FirstOrDefault();
  if (below == null) { return; }
  var p = below.Position;
  below.Position = m.Position;
  m.Position = p;
  UnitOfWork.Commit();
}
```

⚠ **Gaps and ties are harmless.** Only the relative order is ever read, so nothing has to keep the positions dense
and nothing has to renumber after a delete. Reach for a renumbering pass only if you have a reason to — and note
that it is a write per row, where a swap is two.

### The arrows at the ends     {#the-ends}
The first row has nothing above it and the last has nothing below. Those two presses reach the bare `return` above,
and a button that accepts a click and does nothing is indistinguishable from a broken app — so **say so on the
control** rather than in the action. `osy lint` catches this shape under its own rule for a control that accepts a
click and does nothing (`osy lint --rules` lists it by name) — but the disabled/hidden ends here are the fix, not
this page's own topic.

```osy title="the page — and the two arrows that are honest about their ends" test app=ui-reordering
[Page("/")]
[AllowAnonymous]
[Render(CSR)]
[Title("The programme")]
component Programme() {
  string draft = "";

  // In the CONDUCTOR's order — the stored one.
  live var running = Movement.OrderBy(m => m.Position).ToList();

  action Add() { if (draft != "") { AddMovement(draft, 0); draft = ""; } }
  action Up(Movement m) { MoveUp(m); }
  action Down(Movement m) { MoveDown(m); }

  render {
    Stack(gap: 4, p: 6) {
      Row(gap: 2) {
        Field("Movement", value: draft);
        Button("Add", onPress: Add);
      }
      Stack(gap: 2) {
        foreach (var m in running) {
          Row(gap: 2) {
            Text(m.Name);
            Spacer();
            IconButton("Move up", onPress: () => Up(m),
                       disabled: m.Position == running.First().Position) { Icon(Icons.ChevronUp); }
            IconButton("Move down", onPress: () => Down(m),
                       disabled: m.Position == running.Last().Position) { Icon(Icons.ChevronDown); }
          }
        }
      }
    }
  }
}
```

### When the list is FILTERED   {#filtered}
The page above draws every row, so the guard and the action are asking about the same list without having to think
about it. **The moment a filter hides some rows, they are two different lists, and the page is wrong in a way that
looks like nothing happening.**

Hide the done rows, and the top VISIBLE row's "up" is still enabled — its neighbour is a hidden row. Press it and
the two swap, off screen, and the screen does not move. `osy lint` reports this from both sides:
`ui-row-guard-reads-the-unfiltered-list` when the GUARD reads the whole list, and
`ui-guard-and-action-disagree-about-the-list` when the ACTION does.

**One list answers both questions. Bind it once, and let the guard, the loop and the action all read it:**

```osy title="a filtered order — one list, three readers" test app=ui-reordering-filtered
entity Movement { [Required, MaxLength(120)] string Name; int Position; bool Done;
  security { allow read, create, update, delete when IsAnonymous || IsAuthenticated; } }

[Page("/filtered")]
[AllowAnonymous]
[Render(CSR)]
component Shortlist() {
  bool onlyLeft = true;

  live var all = Movement.OrderBy(m => m.Position).ToList();
  // THE one list. The guard, the loop and both actions read THIS — nothing reads `all` again.
  live var shown = onlyLeft ? all.Where(m => !m.Done).ToList() : all.ToList();

  action Toggle() { onlyLeft = !onlyLeft; }

  // The neighbour comes from `shown`, so "up" means the row ABOVE THE ONE YOU CAN SEE.
  action Up(Movement m) {
    int i = shown.IndexOf(m);
    if (i <= 0) { return; }
    var above = shown[i - 1];
    int p = m.Position; m.Position = above.Position; above.Position = p;
    UnitOfWork.Commit();
  }

  render {
    Stack(gap: 4, p: 6) {
      Row(gap: 2) { Button(onlyLeft ? "Show all" : "Only what's left", onPress: Toggle, tone: Tone.Primary); }
      Stack(gap: 2) {
        foreach (var m in shown) {
          Row(gap: 2) {
            Text(m.Name);
            Spacer();
            // The edge is computed over `shown` too — the same list the action will walk.
            IconButton("Move up", onPress: () => Up(m), disabled: m == shown.First()) { Icon(Icons.ChevronUp); }
          }
        }
      }
    }
  }
}
```

⚠ **`shown` is a `live var`, not a render-local.** An action cannot see a local declared inside `render`, so a
filtered list that only exists there forces the action to re-read the table — which is the disagreement above,
arrived at from the other direction.

### Testing it                 {#testing}
The assertion that matters is about the **order relation**, not about a rendered string: a list sorted by name
would render the same words. Seed two rows whose stored order is *not* alphabetical, move one, and read the
positions back.

⭐ **Two different claims, and a reordering feature wants both.** `Assert.Before(a, b)` says *b's row renders below
a's* — the thing the person actually sees — while comparing stored `Position` values says the DATA moved. They can
disagree: a swap can commit and the screen not change, which is the whole failure a reordering page has. Assert the
screen first, because that is the promise; assert the field when you want to pin which row got which number.
`Assert.Before` takes the ROWS, not text they render — see [Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/) `#order`.

```osy title="the order survives a shuffle, and it is not alphabetical" test app=ui-reordering
[Test]
void a_movement_can_be_moved_up_and_the_order_is_mine() {
  Ui.Visit("/");
  Ui.Fill("Movement", "Overture"); Ui.Click("Add");
  Ui.Fill("Movement", "Adagio");   Ui.Click("Add");

  // Entered in the intended order — Overture before Adagio, which is NOT alphabetical.
  var overture = Movement.Single(m => m.Name == "Overture");
  var adagio   = Movement.Single(m => m.Name == "Adagio");
  Assert.True(overture.Position < adagio.Position);

  Ui.Click("Move up", within: adagio);

  Assert.True(Movement.Single(m => m.Name == "Adagio").Position
              < Movement.Single(m => m.Name == "Overture").Position);

  // …and the SCREEN, which is the claim the person would make. A swap that commits without moving the row passes
  // the assertion above and fails this one.
  Assert.Before(adagio, overture);
}
```

⚑ **`within: <the row>`** is how a per-row button is addressed when every row carries one with the same label. Pass
the ROW, not a word it renders.

## Examples       {#examples}
Every fence above is compiled by the docs gate. Taken together they are the whole feature: the stored field, the
append, the two swaps, the page whose arrows are honest at the ends, and the test that proves the order is the
person's rather than the alphabet's.

## See also       {#see-also}
- [OrderBy / ThenBy](https://osysharp.com/reference/query/ordering/) — `OrderBy` over a field the record already has, which is the other case
- [Sorting by a column the user picks](https://osysharp.com/reference/ui/sort-by-column/) — letting the READER re-sort a table, which is a different question from the order the data carries
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — writing and committing from a page


---

<!-- https://osysharp.com/reference/ui/shell/ -->

# App shells

> An app shell is the frame around every page: a brand, a navigation tree, the signed-in person and a menu behind them, a search slot and a place for a page's own actions. You declare that ONCE, as an `AppChrome`, and hand it to the shell you want. Every shell takes the same value and the same slots, so changing arrangement is one word and you lose nothing but the layout.

<!-- id: ui-shell · area: ui · stability: stable · html: https://osysharp.com/reference/ui/shell/ -->

## Summary        {#summary}

A shell is the frame every page renders inside. You declare what it holds **once** — in your `[Layout]` — and hand
that one value to whichever arrangement you want:

```osy title="a whole app's chrome, and the shell that arranges it" test app=ui-shell
[Layout]
[AllowAnonymous]
component AppShell() {
  bool accountOpen = false;
  string accountName = "Olivia Rhye";

  action OpenAccount() { accountOpen = true; }
  action CloseAccount() { accountOpen = false; }
  action Appearance() { Theme.Toggle(); }
  action SignOut() { Session.SignOut(); }
  action Palette() { }
  action Inbox() { }
  action Help() { }

  render {
    SidebarShell(new AppChrome {
      Product = "Expensely",
      Tagline = "Finance operations",
      Mark = Icons.Chart,
      Home = "/",
      User = new ShellUser {
        Name = "Olivia Rhye",
        Secondary = "Finance manager",
        Initials = "OR",
        Menu = [
          new MenuAction { Label = "Account", Icon = Icons.User, OnPress = OpenAccount },
          new MenuAction { Label = "Appearance", Icon = Icons.Eye, OnPress = Appearance },
          new MenuAction { Label = "Sign out", Icon = Icons.Logout, OnPress = SignOut, Tone = Tone.Danger, Divided = true },
        ],
      },
      Nav = [
        new NavItem { Label = "Dashboard", To = "/", Icon = Icons.Home },
        new NavItem { Label = "Approvals", To = "/approvals", Icon = Icons.CheckCircle, Badge = "12", BadgeTone = Tone.Warning },
        new NavItem { Kind = NavKind.Section, Label = "Administration", Children = [
          new NavItem { Label = "Settings", To = "/settings", Icon = Icons.Gear, Children = [
            new NavItem { Label = "People", To = "/settings/people", Icon = Icons.Users },
            new NavItem { Label = "Billing", To = "/settings/billing", Icon = Icons.Tag, Children = [
              new NavItem { Label = "Invoices", To = "/settings/billing/invoices", Icon = Icons.File, Children = [
                new NavItem { Label = "Drafts", To = "/settings/billing/invoices/drafts", Icon = Icons.Pencil },
              ] },
            ] },
          ] },
        ] },
      ],
    }) {
      Outlet(retain: 8);
      slot search { ShellSearch("Search requests, people or departments", onPress: Palette); }
      slot actions { ShellCountButton("Notifications", 3, onPress: Inbox) { Icon(Icons.Bell, size: 18); } }
      slot railFoot {
        ShellFootCard("Need a hand?", "Read the expense guides, or ask us.", "Visit help centre", onPress: Help);
      }
      slot aside {
        if (Navigation.CurrentPath == "/approvals") {
          ShellAside("Waiting on you") {
            Text("Beside the page", fontWeight: FontWeight.Semibold);
            Text("Three requests need a decision today.", fontSize: FontSize.Caption, color: Colors.TextMuted);
          }
        }
      }
    }
    if (accountOpen) {
      Dialog("Account", onDismiss: CloseAccount) {
        Field("Display name", value: accountName);
        slot actions { Button("Save", onPress: CloseAccount, tone: Tone.Primary); }
      }
    }
  }
}

[Page("/")] [Layout(AppShell)] [Title("Dashboard")] [Render(CSR)] [AllowAnonymous]
component Home() {
  render {
    PageHead("Dashboard", subtitle: "What is waiting on you.");
    Card("This month") { Text("Nothing needs you right now."); }
  }
}

[Page("/approvals")] [Layout(AppShell)] [Title("Approvals")] [Render(CSR)] [AllowAnonymous]
component Approvals() {
  render {
    PageHead("Approvals");
    Card("Waiting") { Text("Three requests need a decision today."); }
  }
}

[Page("/settings")] [Layout(AppShell)] [Title("Settings")] [Render(CSR)] [AllowAnonymous]
component Settings() { render { PageHead("Settings"); } }

[Page("/settings/people")] [Layout(AppShell)] [Title("People")] [Render(CSR)] [AllowAnonymous]
component People() { render { PageHead("People"); } }

[Page("/settings/billing")] [Layout(AppShell)] [Title("Billing")] [Render(CSR)] [AllowAnonymous]
component Billing() { render { PageHead("Billing"); } }

[Page("/settings/billing/invoices")] [Layout(AppShell)] [Title("Invoices")] [Render(CSR)] [AllowAnonymous]
component Invoices() { render { PageHead("Invoices"); } }

[Page("/settings/billing/invoices/drafts")] [Layout(AppShell)] [Title("Drafts")] [Render(CSR)] [AllowAnonymous]
component Drafts() { render { PageHead("Drafts"); } }
```

`SidebarShell` is the arrangement. `TabbedShell`, `RailShell` and `FocusedShell` take the **same** `AppChrome` and
the **same** slots, so switching is that one word — see [what each arrangement does](#arrangements).

## Signature      {#signature}

```osy syntax
SidebarShell(chrome)  ·  TabbedShell(chrome)  ·  RailShell(chrome)  ·  FocusedShell(chrome)

class AppChrome {
  string   Product;      // the product's name
  string   Tagline;      // a second line — the workspace, the tenant, the environment
  Icons?   Mark;         // the brand mark as a GLYPH — yours or a built-in; unset draws the product's initial
  string   MarkSrc;      // …or as an IMAGE — a full-colour logo, a tenant's own. Wins over `Mark`
  string   Home;         // where the brand lockup goes when pressed
  NavItem[] Nav;         // the navigation TREE
  ShellUser User;        // who is signed in, or null for nobody
  bool     Loading;      // draw skeleton nav rows instead of an empty column
}

class NavItem {
  string    Label;  string To;  Icons Icon;   // a built-in, or any `.svg` your app ships
  NavItem[] Children;    // non-empty ⇒ a GROUP, unless `Kind` says Section
  NavKind   Kind;        // Link (default) · Group (inferred) · Section
  string    Badge;  Tone BadgeTone;
  bool      Exact;       // match this route exactly, never as a prefix
}

class ShellUser  { string Name, Secondary, Initials, AvatarSrc;  MenuAction[] Menu; }
class MenuAction { string Label;  Action OnPress;  Icons Icon;  Tone Tone;  bool Divided; }

// the slots — the same six on every shell. A slot reserves a POSITION; the control you put in it
// brings the chrome, which is why an unfilled slot costs nothing.
(default)  the routed page — your `Outlet(retain: n)`
search     a global search affordance   · `ShellSearch(placeholder, onPress, hint)`
actions    top-bar controls             · `ShellCountButton(label, count, onPress) { Icon(…); }`
railFoot   pinned at the foot of a rail · `ShellFootCard(title, body, actionLabel, onPress)`
aside      a right rail                 · `ShellAside(label) { … }` — a column beside the page where
                                          there is room, a section under it where there is not
```

## Description    {#description}

### What an app declares, and what a shell decides   {#chrome}

`AppChrome` is **what**; the shell is **how**. The app says "these are my sections, this is who is signed in, this
is my product"; the shell decides whether that becomes a left rail, a tab strip, an icon strip or nothing at all.

That split is why the nav is **data** and not slot children. A `Nav { NavItem(…) NavItem(…) }` block reads nicely
and cannot be REARRANGED — the child components would decide their own layout, so a tabbed shell handed a rail's
children renders a rail. A shell has to be able to walk the tree, flatten it, nest it, or drop it. Everything a
shell genuinely cannot rearrange — a search box, a page's own buttons — stays a slot.

⚠ **Build the chrome at the CALL SITE, not in a `live var`.** A `MenuAction` carries an `Action`, and an action may
only be set where the platform can run it. Hoisting the whole `new AppChrome { … }` into a `live var` is refused,
and the refusal says so.

### How a nav row knows it is the page you are on   {#nav}

Active state is **derived from the route**, never passed in per page. Getting this wrong is the single thing that
makes a shell feel fake, so the rule is stated once, in `AppChrome.CurrentRoute`, and every shell uses it:

- a row **matches** `path` when `To == path`, or when `path` starts with `To + "/"` — so `/orders` claims
  `/orders/42` and never `/orders-archive`;
- **`/` is exempt from prefix matching.** The home route is a prefix of every path in the app, so without this Home
  is lit on every page. Set `Exact = true` on any other row that must not claim its own children;
- exactly one row is **current**: the LONGEST match across the whole tree wins. With `/settings` and
  `/settings/users` both in the nav, a per-row test lights *both* on the child's page, and two current rows read as
  a bug;
- every ancestor of the current row is **on the trail** — a lighter treatment, and it OPENS.

**How deep can a nav go?** As deep as you like. A shell draws three levels of INDENT — section, group, leaf — and
flattens everything below the third to it, so a row five levels down appears beside its parent rather than
disappearing. Active state is computed over the whole tree at any depth, by the same walk that decides what renders,
so the row you are standing on is marked wherever it sits.

```osy title="one row current, its ancestors open — the rule, not a per-page flag" syntax
/settings/policy/chains   ⇒  Administration  section, shown
                             Settings        group, on the trail, OPEN
                             Policy          group, on the trail, OPEN
                             Approval chains CURRENT
```

⭐ **A deep link arrives with its ancestors already open.** Landing straight on `/settings/policy/chains` — from a
bookmark, from an email — must not leave the current row hidden inside a collapsed group. A group is open when it
holds the current route; a reader who then closes it is remembered, so it does not spring back.

### The person, and the menu behind them   {#identity}

`ShellUser` is what a shell shows. The app supplies it: `Session.CurrentUser` is the obvious source, but which of
your own columns is the NAME and which is the ROLE is your app's question, not the kit's.

`Menu` is the app's too — the shell never hardcodes which entries exist, so an app with no billing has no Billing
row. Each `MenuAction` carries a verb, and a verb may open a **dialog**:

```osy title="a menu item that opens a dialog" syntax
new MenuAction { Label = "Account", Icon = Icons.User, OnPress = OpenAccount }

action OpenAccount() { accountOpen = true; }     // …and render a `Dialog` beside the shell
```

The shell closes its own menu and drawer *before* running your action, so a dialog opens over a clean page. Render
the `Dialog` next to the shell in your layout, not inside it — nothing the rail does with transforms or overflow can
then clip it.

⚠ **A user with no `Menu` gets a LABEL, not a button.** A chip that opens nothing is an affordance that lies.

### What a page contributes, and what it cannot   {#slots}

A page renders inside an `Outlet`, so it **cannot fill a slot of the shell that hosts it**. Two consequences worth
knowing before you design around them:

| you want | where it goes |
|---|---|
| the bar's title | nowhere — the shell reads the page's own `[Title("…")]` (or its last `Navigation.SetTitle`) |
| a page's own actions | on the page, in its `PageHead(…)` |
| a right rail | the layout's `slot aside`, keyed on the route, holding a `ShellAside(…)` — or the page's own two-column `Row` |
| global search, notifications, help | the layout's `search` / `actions` / `railFoot` slots |

The title is the one worth pausing on: a page names itself once, with `[Title]`, and every shell's bar follows. There
is nothing to keep in sync, and a page that declares no title leaves the bar empty rather than inventing one from
the URL.

### What changes at each width?   {#bands}

A shell is not one layout that stretches. `SidebarShell` reads the band **once per render** and everything follows
from it — the rail's width, whether a group is an accordion or a flyout, whether a menu is a dropdown or a sheet,
whether `aside` is a column or a section:

| band | width | the design |
|---|---|---|
| **compact** | < 768 | the rail is an off-canvas DRAWER over a scrim; a menu button in the bar; menus are bottom SHEETS within thumb reach; `aside` stacks under the page |
| **cozy** | 768+ | the rail DOCKS as a 60px icon strip — always visible — and a group opens as a FLYOUT beside it; one content column |
| **wide** | 1100+ | the rail opens to 264px with labels, badges and section headings; the page and its `aside` sit SIDE BY SIDE |

⭐ **The wide band is not the narrow one stretched.** A single centred column at 1440px is a phone layout on a
desktop: dead space, and one thing visible at a time. A wide screen should show MORE — that is what it is for.

⭐ **And the compact band is not the wide one squeezed.** Every target is at least `Length.Touch` (44px), nothing
load-bearing is reachable only by hovering, and overlays are sheets rather than dropdowns pinned to a corner a thumb
cannot reach.

### What each arrangement does with the same declaration   {#arrangements}

| shell | nav on a pointer | nav on a phone | identity | when to reach for it |
|---|---|---|---|---|
| `SidebarShell` (this page) | a persistent left rail; groups disclose in place | an off-canvas drawer | a chip in the top bar | the default for anything with more than about five sections |
| [TabbedShell](https://osysharp.com/reference/ui/shell-tabbed/) `TabbedShell` | a horizontal strip under the brand row; a group becomes a dropdown | a **bottom tab bar**, plus a "More" sheet | a chip in the brand row | a handful of peer sections, and a phone-heavy audience |
| [RailShell](https://osysharp.com/reference/ui/shell-rail/) `RailShell` | icons only, always; labels arrive on hover and focus | a labelled off-canvas drawer | **at the rail's foot** | a tool where the canvas is the product |
| [FocusedShell](https://osysharp.com/reference/ui/shell-focused/) `FocusedShell` | **none** — the `Nav` becomes an ordered set of STEPS | the same, as "Step 2 of 4" | minimal, beside the exit | a wizard, a checkout, a reader |

⚠ **`FocusedShell` drops NAVIGATION, not the nav DATA**, and the distinction is the whole reason switching to it
costs nothing. A focused flow has one job and a rail beside it is an invitation to leave — so there is no rail. But
the same `Nav` you already declared is re-read as a linear FLOW: a top-level entry is a step, in order, and its
children are that step's parts. You declare nothing new to switch, and nothing is lost switching back.

⚠ **THIS TABLE DESCRIBED FOUR SHELLS WHILE ONE EXISTED**, from 2.3.0 until 2.5.0. Two rows were wrong when the
other three actually shipped and are corrected above: `TabbedShell`'s identity is in the brand row rather than at
the strip's end (the strip needs its full width for tabs), and `FocusedShell` shows the `Nav` as steps rather than
hiding it. Read a version note as a promise until the thing exists.

### The standard a shell has to meet   {#standard}

This is the bar `SidebarShell` was built to, and what the other arrangements are held to. It is here rather than in
a brief because a standard that lives in a brief expires; a downloader forking a shell should be able to read it.

**Responsive** — three bands, each designed on its own terms. Judge each width by "is this the best layout for a
screen this size", never by "does it survive being resized to this".

**Touch** — every interactive target at least `Length.Touch`; `Length.TouchDense` only where the neighbours are the
same control and a miss is harmless. Nothing load-bearing behind a hover: a hover-only disclosure does not exist on
a touch screen, so a group needs a tap path at every width.

**Overlays** — a drawer sits over its own scrim (`ZIndex.Drawer`) and under a dialog (`ZIndex.Modal`). Opening one
overlay closes the others the shell owns, so nothing is left hanging underneath. On a phone a menu is a bottom
sheet, and a sheet still traps focus, still closes on Escape, and still returns focus where it came from.

**Active state** — derived from the route by longest match, never hand-passed. Exactly one row current; ancestors on
the trail and open; a deep link arrives open.

**Landmarks and keyboard** — `nav`, `banner` and `main` regions, each NAMED (a page with two unnamed `nav` regions
is unnavigable). A visible focus ring on every control — replace the default, never remove it. `current:` on the
current nav row, so a screen reader announces "current page" and not just a colour.

**Overflow** — a long product name, a 35-character nav label, twenty rows, a user with no avatar and an empty nav
all leave the layout intact. Truncate with intent: `TextOverflow.Ellipsis` needs `WhiteSpace.Nowrap` and
`Overflow.Hidden` beside it or there is nothing to clip. **The page body never scrolls sideways at any width** —
`minW: "0"` on the work column is what lets a wide table scroll inside it instead of shoving the shell.

**Images** — the brand mark, a nav glyph and an avatar are the three, and an app can supply ALL of them: one
vocabulary (`Icons`, which holds the app's own `.svg` files beside the built-ins) for anything drawn as a glyph, one
`…Src` string for anything whose address is only known at runtime, and a derived fallback for each so nothing is
required to get a shell. ⚠ A shell that lets an app choose its user's photograph but not its own logo has the
priority backwards — that was true here until 2.4.0, and it is written down so the next arrangement does not
inherit it.

**Both themes** — every colour from a token, never a literal, and dark checked rather than assumed. Borders and
elevation are where a dark theme fails first.

**Hierarchy** — primary, secondary and destructive actions visually distinct; nav sections carry group labels; icons
consistently sized and optically centred. Motion from `Motion.Fast` / `Motion.Normal`: a shell with no transitions
feels dead, one with slow transitions feels cheap.

**Loading and empty** — the shell owns the states it can own. `chrome.Loading` draws skeleton nav rows rather than an
empty column, and an empty `Nav` draws a sentence rather than a blank rail.

**A slot is a POSITION, never chrome** — and this one is a rule rather than a preference, because breaking it is
invisible. A shell reserves *where* optional content goes; the width, the border, the fill and the sticky box arrive
with the content, in the control the app puts there — `ShellSearch`, `ShellCountButton`, `ShellFootCard`,
`ShellAside`. A shell that draws the box itself draws it whether or not anyone filled the slot: `SidebarShell` did
exactly that for `aside` and cost every app that ignored it **26% of a 1280 screen**, a full-height hairline and a
band of `Surface`, for an empty region.

⚠ **And you cannot fix that by asking whether the slot was filled.** The renderer collapses an element whose whole
content is an unfilled named slot, but it deliberately stops short of a container whose contents can *arrive* —
collapsing one would move layout under the reader. An aside is exactly that container: it is keyed on the route, and
a route-keyed fill is an `if`, which leaves an anchor whichever way it goes. So a "was it filled?" answer is
computed once, at build, and is wrong the moment the reader navigates. Let the caller bring the box and the question
never arises.

### It is a floor, not a ceiling   {#forking}

Every shell is ordinary Osy# whose source ships. `osy kit SidebarShell` prints it, and declaring a component of the
same name in your own app shadows it whole — nothing here is privileged and nothing is unforkable. The interior
pieces are marked `[Part]`, so they stay out of `osy kit`'s catalogue while remaining just as forkable.

Fork when you outgrow it; compose first. Most of what a shell needs varying is already a slot or a field.

### The three images a shell draws   {#images}

A shell shows a **brand mark**, a **glyph per nav row and menu row**, and the signed-in person's **avatar**. There
is one vocabulary and one fallback rule for all of them, and both are worth learning once:

| | what you set | falls back to |
|---|---|---|
| brand mark | `Mark = Icons.X` (a glyph) or `MarkSrc = "…"` (an image) | the product's first letter |
| nav row · menu row | `Icon = Icons.X` | `Icons.Folder` · `Icons.ChevronRight` |
| the person | `AvatarSrc = "…"` | `Initials`, then a person glyph |

⭐ **`Icons` is YOUR vocabulary, not a kit one.** Every `.svg` under your app's `icons` glob is a member of it
beside the built-ins, under the name of the file — `icons/receipt.svg` is `Icons.Receipt` — and the shell holds
exactly that type. So an app icon and a built-in are written the same way, in the same field, and nothing about a
nav row prefers the ones the platform happens to ship:

```osy title="an app's own two glyphs, in the brand and in the nav" test app=file-manager
// `Icons.Logo` and `Icons.Chev` are this app's OWN files — `model/icons/logo.svg` and `model/icons/chev.svg`.
// Neither is a built-in, and neither is spelled differently from `Icons.Home` two lines down.
[Layout]
[AllowAnonymous]
component BrandedShell() {
  render {
    SidebarShell(new AppChrome {
      Product = "Ledger", Home = "/", Mark = Icons.Logo,
      Nav = [
        new NavItem { Label = "Files", To = "/", Icon = Icons.Home },
        new NavItem { Label = "Archive", To = "/archive", Icon = Icons.Chev },
      ],
    }) { Outlet(retain: 4); }
  }
}
```

⚠ **A GLYPH TAKES THE SHELL'S COLOUR; AN IMAGE KEEPS ITS OWN.** `Mark` is drawn like every other icon — one colour,
inherited — on the brand tile. A logo with colours of its own is not a glyph, so it goes in `MarkSrc`, which
replaces the tile rather than tinting what you put on it.

⭐ **`MarkSrc` is also how a per-tenant logo works.** It is a plain `src`, so it can come from a row: a white-label
app reads its tenant's logo at render time and hands it over, exactly as it already does for a user's photograph.
That is the reason `AvatarSrc` stays a `string` rather than folding into `Icons` — a design asset is known when you
compile, and a person's photograph is not.

### What it cannot do yet   {#limits}

⚠ **A slot cannot hold the brand mark.** `Mark`/`MarkSrc` cover a glyph and an image; an arbitrary node there — a
full-colour inline `Svg(Art.X)`, a wordmark you want to lay out yourself — needs a fork of `ShellBrand`, which
`osy kit ShellBrand` prints for you.

## See also       {#see-also}

- [TabbedShell](https://osysharp.com/reference/ui/shell-tabbed/) — top tabs on a pointer, a bottom tab bar on a phone
- [RailShell](https://osysharp.com/reference/ui/shell-rail/) — a permanent icon rail with flyout labels
- [FocusedShell](https://osysharp.com/reference/ui/shell-focused/) — one task, no navigation, the nav read as progress
- [layout primitives](https://osysharp.com/reference/ui/layout/) — `Stack`, `Row`, `Box`, and how `gap`/`align`/`justify` work
- [routes and pages](https://osysharp.com/reference/ui/routing/) — `[Page]`, `[Layout]`, `Outlet` and retention
- [Navigation](https://osysharp.com/reference/ui/navigation/) — `Navigation.Routes`, `[Title]`, and what a shell reads from them
- [Dialog.Open / Dialog.Ask / Dialog.Confirm / Dialog.Discard](https://osysharp.com/reference/ui/dialogs/) — `Dialog.Open` and the unit-of-work choice a modal makes
- [accessibility](https://osysharp.com/reference/ui/accessibility/) — landmarks, `role:`, `current:` and the naming props
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — the rest of the controls a shell frames


---

<!-- https://osysharp.com/reference/ui/render-calls/ -->

# Calling helpers from render

> A render expression may call a PURE client helper — a component `method`, a client class method, a top-level function — and render the value it returns (`Text(Twice())`). The compiler PROVES the call is safe: client-side, writes no state, invokes no effect. A call that reaches the server, writes state, or runs an effect is a precise compile error, so a render expression stays synchronous and pure by construction.

<!-- id: ui-render-calls · area: ui · stability: stable · html: https://osysharp.com/reference/ui/render-calls/ -->

## Summary        {#summary}
Render is where you turn data into what's on screen — and sometimes the value you want to show needs a small
computation. Write it as a **component `method`** (or any pure client helper) and call it right in the render
expression:

```osy syntax
component Cart() {
  live var items = LineItem.ToList();
  decimal Subtotal() => items.Sum(i => i.Price * i.Qty);   // a pure helper over component state

  render { Text(Subtotal()); }                              // call it in render — natural C#
}
```

The call runs **in the browser, synchronously**, as part of rendering — and re-runs (repainting only that slot) when a
value the helper read changes. See [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) for the fine-grained update model.

## Signature      {#signature}
```osy syntax
render { Text(Helper(arg)) }   // Helper is a component method, a client class method, or a top-level function
```

A render call is legal **iff** it is **client-side (never suspends) ∧ writes no state ∧ invokes no effect** — the
compiler checks all three.

## Description    {#description}

### What you can call   {#allowed}
- A **component `method`** — `int Twice() => n * 2;` then `Text(Twice())`. It reads the component's own state through
  `this` implicitly, exactly as an `action` does.
- A **client class method** — `money.Formatted()` on a `class` value whose method body is client-runnable.
- A **top-level function** whose body is client-runnable — an ordinary `string Shout(string s) { … }`. You mark
  nothing: the compiler decides where it can run from what it touches, and refuses the call if the answer is the
  server.

Pure stdlib calls (`Convert.ToString`, `Enum.Label`, …) have always been render-slot material; this extends the same
door to **your own** helpers, so you don't have to hoist a one-line computation into a `live var` just to name it.

### What the compiler refuses — and why   {#refused}
A render expression must run **synchronously and purely** on the client (it can re-run many times as data changes, and
it may not block or cause side effects). So a call that breaks any of the three rules is a **compile error that names
the offending reach**:

| The call… | Error |
|---|---|
| reaches a **data read** or a **server function** | `… cannot be called from a render expression — it reaches server-side work …` |
| **writes** the component's own state | `… it assigns the component's own state (`count`) …` |
| invokes an **effect** (`Log`, `Http`, …) | `… it invokes the effect `Log.Information` …` |

The fix is always the same: move the imperative call into an `action` or `on mount`, store what it produces in state,
and render *that*. A data read is a `live var` query ([component](https://osysharp.com/reference/ui/component/)); a one-time load is [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/).

### Why this is safe by construction   {#purity}
The fine-grained renderer re-runs a slot's expression inside a tracking effect whenever a value it read changes
([The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/)). "A render expression is pure" therefore isn't a convention you must remember — it's a compile-time
guarantee, which is what lets you call your helpers in the view without ever creating a render loop.

## Examples       {#examples}
A cart line renders a per-row subtotal via a pure helper — no `live var` needed for the one-liner:

```osy title="render-helper" test app=ui-render-calls
entity Product { string Name; decimal Price; }

[Page("/catalog")]
[Render(CSR)]
component Catalog() {
  live var products = Product.ToList();
  decimal WithTax(decimal price) => price * 1.25m;   // a pure client helper

  render {
    foreach (var p in products) {
      Text(p.Name);
      Text(WithTax(p.Price));                          // called in render — the compiler proves it pure
    }
  }
}
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the `method` member, and the `live var` query you'd reach for when a value needs the server.
- [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) — the fine-grained model that makes "render is pure" a structural requirement.
- [on change](https://osysharp.com/reference/ui/on-change/) — the reactive block for pushing a value *out* of the component (the opposite direction).
- [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) — `on mount`, for one-time imperative setup a render call may not do.


---

<!-- https://osysharp.com/reference/ui/canvas/ -->

# Canvas

> A drawing surface, and the verbs that paint on it. Put a `Canvas` in a render block, call `Draw.*` from an `on frame` body, and the picture is redrawn every frame — a game, a visualisation, a custom chart, anything the box-shaped render vocabulary cannot express. The drawing code is ordinary Osy#: ordinary loops, ordinary helper methods, ordinary state.

<!-- id: ui-canvas · area: ui · stability: preview · html: https://osysharp.com/reference/ui/canvas/ -->

## Summary        {#summary}
Everything else the render vocabulary draws is a **box**. That reaches further than it sounds — a Tetris well, a
bar chart and even a textured raycaster are all boxes — but a box cannot vary **within itself**, so lighting that
falls off down a wall, a sprite clipped by the thing in front of it, or a textured floor have no spelling.

`Canvas` is the surface for those. It is **immediate mode**: there is no scene, no retained objects and nothing to
keep in sync. Each frame you clear it and draw what should be there now.

The drawing happens in **Osy#**, in your `on frame` body — not in a callback handed to a JavaScript library. That is
the point of the design: a game's inner loop stays in the same language as the rest of the app.

## Signature      {#signature}
```osy syntax
Canvas(w: 640, h: 400)              // the surface. `w`/`h` are the drawing BUFFER, in canvas pixels

Draw.Clear(color)                   // fill the whole canvas
Draw.Rect(x, y, w, h, color)        // a filled rectangle, from its top-left corner
Draw.Line(x1, y1, x2, y2, color)    // a 1px stroked segment
Draw.Line(x1, y1, x2, y2, color, width)
Draw.Circle(x, y, radius, color)    // a filled disc, from its CENTRE
Draw.Text(text, x, y, color, size)  // a string, from its TOP-left; `size` in canvas pixels
Draw.Image(wall, dx, dy, dw, dh)    // a TEXTURE the app ships, into a destination rectangle
Draw.Image(wall, sx, sy, sw, sh, dx, dy, dw, dh)  // a SOURCE rectangle of it, into a destination one
Draw.Image(url, …)                  // the same two forms over a runtime url — see [textures](https://osysharp.com/reference/ui/textures/)
Draw.Pixels(buffer, w, h, dx, dy)   // a w-by-h buffer of packed 0xRRGGBB colours, one canvas pixel each
Draw.Pixels(buffer, w, h, dx, dy, dw, dh)   // …STRETCHED into a destination rectangle

Draw.Push()                         // save the current transform
Draw.Pop()                          // put back the one the matching Push saved
Draw.Translate(x, y)                // move the origin
Draw.Rotate(radians)                // turn everything drawn after it, about the origin
Draw.Scale(sx, sy)                  // resize it; a NEGATIVE factor mirrors
Draw.Reset()                        // back to plain canvas coordinates, stack and all

Gradient g = Draw.Gradient(x0, y0, x1, y1, colorA, colorB)   // a LINEAR fill, running between two points
Gradient r = Draw.Radial(x, y, radius, colorA, colorB)      // a RADIAL fill, running out from a centre
Draw.Rect(0, 0, 640, 420, g)        // a gradient goes wherever a colour goes

var sky = Draw.Surface(640, 420)    // an OFFSCREEN surface — hold it in a field, build it once
Draw.Into(sky)                      // …the verbs above now paint on it
Draw.Screen()                       // …and back to the component's Canvas
Draw.Image(sky, dx, dy, dw, dh)     // blit it, with the same two forms a texture takes
```

## Description    {#description}

### What goes in a frame body — clear, then draw   {#a-frame}
A canvas keeps what you drew last time, so a frame normally starts by clearing it and then draws everything that
should be visible now. State lives in the component, exactly as it does without a canvas.

```osy title="a disc that bounces" test app=arcade-paint
[Page("/bounce")]
[AllowAnonymous]
component Bounce() {
  double x = 60;
  double y = 60;
  double vx = 210;
  double vy = 160;

  on frame (double dt) {
    x = x + vx * dt;
    y = y + vy * dt;
    if (x < 24) { x = 24; vx = 0 - vx; }
    if (x > 296) { x = 296; vx = 0 - vx; }
    if (y < 24) { y = 24; vy = 0 - vy; }
    if (y > 176) { y = 176; vy = 0 - vy; }

    Draw.Clear("#0B0616");
    Draw.Circle(x, y, 22, "#22E5FF");
  }

  render { Canvas(w: 320, h: 200); }
}
```

Every position is scaled by `dt`, so the disc crosses the surface in the same wall-clock time whatever frame rate
the display runs at. See [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) — that is a property of `on frame`, not of the canvas.

### Coordinates are canvas pixels    {#coordinates}
`x` grows right, `y` grows **down**, and the origin is the top-left corner — the convention every 2D drawing API
uses. The unit is the buffer declared by `Canvas(w:, h:)`, not CSS pixels and not a normalised space, so a loop
index is an `x` directly:

```osy title="one draw call per screen column" test app=arcade-paint
[Page("/columns")]
[AllowAnonymous]
component Columns() {
  double t = 0;

  on frame (double dt) {
    t = t + dt;
    Draw.Clear("#0B0616");
    for (var i = 0; i < 160; i = i + 1) {
      var h = 20 + i;
      Draw.Rect(i * 2, 200 - h, 2, h, i % 2 == 0 ? "#2A1B3D" : "#3A2551");
    }
  }

  render { Canvas(w: 320, h: 200); }
}
```

### Rotating, moving and scaling what you draw   {#transforms}
Every shape above is drawn **axis-aligned**: `Draw.Rect` takes a rectangle and `Draw.Image` takes a destination
rectangle, so neither can be turned. The transform verbs are how anything points somewhere.

The idea is that you draw a thing **once, in its own coordinates** — at the origin, facing along +x — and then say
where it is and which way it faces:

```osy syntax
Draw.Push();                 // remember where we were
Draw.Translate(ship.X, ship.Y);
Draw.Rotate(ship.Heading);   // RADIANS, like Math.Cos / Math.Sin
Draw.Line(14, 0, -10, -9, "#22E5FF", 2);     // the ship, at the origin, pointing along +x
Draw.Line(14, 0, -10, 9, "#22E5FF", 2);
Draw.Line(-10, -9, -10, 9, "#22E5FF", 2);
Draw.Pop();                  // and back
```

`Draw.Scale(sx, sy)` resizes the same way, which lets one unit-sized shape serve every size of a thing; a **negative**
factor mirrors, so `Draw.Scale(-1, 1)` is how a sprite faces left.

The verbs compose: a translate inside a translate is relative to the outer one, which is what lets a turret sit on a
tank and turn independently, with each drawn in its own frame.

⚠️ **Angles are radians.** Same as `Draw.Mesh`'s rotations, and same as `Math.Sin` / `Math.Cos` — which is what you
are usually holding anyway. From degrees: `deg * Math.Pi / 180.0`.

#### Every frame starts clean   {#transform-reset}
The transform is **reset before each frame body runs**. That is worth knowing for two reasons:

- A `Draw.Push()` you forget to `Draw.Pop()` is wrong for **that frame only**. Without the reset the world would
  drift a little further every frame, and the symptom — a slow rotation over half a minute — looks like a bug in your
  own maths rather than a missing line. (You are also told, once, when a frame body leaves one unpopped.)
- A camera can be a single line. `Draw.Translate(-camX, -camY)` at the top of the body, with no push and no pop, is
  the whole of it — because the next frame does not inherit it.

`Draw.Reset()` does the same thing **mid-frame**: it drops every transform and every push, which is how a HUD gets
nailed to the screen over a world that is moving.

#### Two verbs that do not follow the transform   {#transform-exceptions}
- **`Draw.Clear`** always clears the whole canvas, whatever the transform is. Clearing "the visible rectangle, moved"
  is never what anyone means, and the leftovers would read as smearing.
- **`Draw.Pixels`**, in its unscaled form, lands in plain canvas coordinates — the browser's pixel blit ignores
  transforms by specification. The **stretched** form (`dw`, `dh`) goes through the image path and does follow them.
  You are told once if you blit under a transform.

```osy title="a ship that points where it is flying" sample=arcade/model/pages/rocks.osy#PaintShip
```

### Gradients are values, not verbs   {#gradients}
`Draw.Gradient` and `Draw.Radial` do not draw anything. They answer a **`Gradient`** — a fill you then pass
wherever a colour string goes:

```osy syntax
Gradient sky;

on frame (double dt) {
  if (sky == null) {
    sky = Draw.Gradient(0, 0, 0, 430, "#4d8fd6", "#a9d2f2", "#ffeccd");
  }
  Draw.Rect(0, 0, 1280, 430, sky);
}
```

**That is why there is no `Draw.GradientRect`.** A gradient is accepted by `Draw.Clear`, `Draw.Rect`,
`Draw.Circle`, `Draw.Line` and `Draw.Text` alike, and by anything added later — because the fill is a value rather
than a second version of every verb.

**Two or more colours, spaced evenly** along the run. `Draw.Gradient` runs between the two points you give, so
`(0, 0, 0, 430)` is vertical and `(0, 0, 640, 0)` is horizontal; `Draw.Radial` runs from a centre outward, which is
what a sun, a glow or a vignette wants. A colour with alpha (`"rgba(255,238,190,0)"`) is how a glow fades into what
is behind it.

**Build it once.** A gradient is fixed in canvas pixels, so hold it in a `Gradient` field — as above, or in
`on mount` — rather than rebuilding it every frame.

### Painting something once — offscreen surfaces   {#surfaces}
A canvas keeps nothing between frames, so everything on screen is redrawn every frame — including the parts that
never change. A starfield of 160 stars is 160 draw commands, which is 9,600 a second for a picture that is identical
each time.

A **surface** is an offscreen canvas you paint once and then blit:

```osy syntax
var sky = Draw.Surface(640, 420);     // a field: built once, used every frame
bool painted = false;

on frame (double dt) {
  if (!painted) {
    Draw.Into(sky);                   // every verb from here paints on `sky`…
    Draw.Clear("#0B0616");            // …including Clear, which clears the SURFACE
    PaintStars();
    Draw.Screen();                    // …and back to the canvas
    painted = true;
  }
  Draw.Image(sky, 0, 0, 640, 420);    // one command instead of a hundred and sixty
  …the moving things…
}
```

`Draw.Image` takes a surface exactly as it takes a texture, in both the whole-image and source-rectangle forms — so
switching a background from shipped art to something you rendered is a change of value, not of call site.

The other thing surfaces buy is a **pre-rendered sprite**: rotating a shape costs the same every frame, and rotating
it once into a surface costs one blit thereafter.

⚠️ **Build a surface once.** It is a real canvas — allocating one per frame is the same mistake as loading an image
per frame. A field, or the first frame, is where one belongs. The largest edge is 4096 pixels.

#### Every frame starts on the screen   {#surface-reset}
As with the transform, the target is **reset before each frame body runs**. A `Draw.Into` you forget to close would
otherwise send every following frame into a buffer, and the screen would simply stop changing — no error, no blank
canvas, a frozen picture that reads as a hung game. Instead it costs you the rest of *one* frame, and you are told
once.

#### What a surface cannot do   {#surface-limits}
The **3D verbs** (`Draw.Camera` / `Light` / `Fog` / `Mesh`) always render on the component's own canvas and refuse to
paint into a surface — call `Draw.Screen()` first. Each surface keeps its **own** transform and its own `Draw.Push`
stack, so a `Draw.Pop` inside one can never restore the screen's; entering a surface gives you a clean coordinate
system.

```osy title="a starfield painted once and blitted every frame" sample=arcade/model/pages/rocks.osy#Starfield
```

### `w:` and `h:` are the resolution, and they are required    {#size}
They set the drawing **buffer** — how many pixels there are to draw on — as well as the element's CSS size, so one
canvas pixel is one CSS pixel by default. To show a fixed resolution larger or smaller, style the element:
`Canvas(w: 320, h: 200, maxW: "100%")`.

Both are **required**, and both must be written as **whole-number literals**. A canvas with no declared size
silently gets 300×150 for its buffer while the element stretches to whatever CSS says, which renders everything
blurred and in the wrong place with nothing to indicate why — so it is refused instead. And because assigning a
canvas's size **clears it**, a size that varied with state would blank the surface at a moment nothing in the code
names; a fixed resolution scaled by CSS is what you want anyway.

### Text draws in the canvas's own font    {#text}
`Draw.Text` uses the font-family the `Canvas` element itself has, so `Canvas(w: 640, h: 400, fontFamily: Font.Display)`
makes drawn text match the `Text(…)` beside it. `y` is the text's **top**, like every other verb's `y` — not its
baseline.

### Images load on first use    {#images}
`Draw.Image` starts loading a url the first time it sees one and draws nothing until it has arrived, so the first
frame that names a new image skips it and every later frame has it. Cache-warm it by drawing it off-screen if the
first frame matters.

The nine-argument form takes a **source** rectangle and a **destination** rectangle, which is how a sprite sheet is
cut up — and how a textured wall is drawn, by stretching a one-pixel-wide slice of a texture to the wall's height.

### Drawing needs a canvas in the same component    {#same-component}
`Draw.*` paints the `Canvas` declared by the component whose body is running. A canvas inside a nested component
belongs to that component, and drawing on it from the outside is refused with a message saying so — it would
otherwise appear to work until the child re-rendered.

### Can a screen reader read a canvas? No   {#accessibility}
Nothing drawn on a canvas is text, an element, or reachable by a screen reader or a keyboard. Use it for what is
genuinely a picture, and put anything a reader must be able to read or press in ordinary atoms around it — a score,
a legend, the controls. A canvas that is decorative needs no description; one that carries information needs the
same information available another way.

### How much can a frame draw?    {#budget}
Re-measured in a real browser, 2026-09-05: roughly **75,000 draw commands** per frame while holding 60fps (90,000
drops to 30), and **more than 300,000** arithmetic steps — the top of the rig, so that ceiling was not found. That is
far past a per-column raycaster and well into per-tile and per-sprite work. If a frame body gets slower than the
display, the frame rate drops rather than frames queueing up.

⚠️ **The draw budget is the browser's, not the language's.** The same measurement with the JS compilation switched
off is identical — 75,000 either way — because what a `Draw.*` call costs is the canvas, and Osy#'s own overhead
around it is already small beside that. The arithmetic budget is where compilation shows, and there the ceiling is
past anything a 2D game needs.

⚑ These are one machine on one day, as the previous figures were; treat them as an order of magnitude rather than a
specification, and measure your own frame if you are close to the edge.

## See also {#see-also}
- [Canvas 3D](https://osysharp.com/reference/ui/canvas-3d/) — the lit, shadowed 3D scene the same canvas can carry: `Draw.Camera`, `Draw.Light`, `Draw.Mesh`
- [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) — `on frame (double dt)`, the clock that drives a canvas
- [component](https://osysharp.com/reference/ui/component/) — component state, which is where a drawing's state lives
- [animation — looping motion with no destination state](https://osysharp.com/reference/ui/animation/) — declarative motion for ordinary elements, which needs no canvas


---

<!-- https://osysharp.com/reference/ui/canvas-3d/ -->

# Canvas 3D

> A lit, shadowed 3D scene on the same `Canvas` the 2D verbs paint. Build meshes once into fields, then each frame place a camera, a sun and some fog, and draw each mesh where it is now. The scene is rendered on the GPU and composited into the canvas, so 2D verbs draw a sky behind it and a score over it in the same frame body.

<!-- id: ui-canvas-3d · area: ui · stability: preview · html: https://osysharp.com/reference/ui/canvas-3d/ -->

## Summary        {#summary}
[Canvas](https://osysharp.com/reference/ui/canvas/) paints flat shapes. A game that wants **depth** — hills that recede, a bird lit from one side, a
shadow under it, haze at the horizon — cannot get it from rectangles at any frame rate, so the canvas carries a
second layer: a **scene** of meshes, drawn with the same immediate-mode discipline as everything else. Nothing is
retained between frames except the meshes themselves; each frame you say where the camera is, where the light
comes from, and where every mesh sits now.

The look is deliberately **low-poly and flat-shaded**: every face is one flat colour, lit by a sun with a soft
shadow, an ambient sky, filmic tone mapping and distance fog. That is the look a few hundred well-placed boxes,
cones and spheres produce best, and it is what runs at 60fps on an ordinary laptop.

## Signature      {#signature}
```osy syntax
Mesh ground = Mesh.Plane(200, 200);            // a field — build a mesh ONCE, in `on mount` or an initializer
Mesh trunk  = Mesh.Cylinder(0.3, 0.4, 2, 8);   // (radiusTop, radiusBottom, height, segments), along y
Mesh crown  = Mesh.Cone(1.4, 3, 8);            // (radius, height, segments), point on top
Mesh rock   = Mesh.Sphere(1, 1);               // (radius, detail 0..4) — an icosphere, 20 × 4^detail faces
Mesh crate  = Mesh.Box(1, 1, 1);               // (width, height, depth), centred on the origin
Mesh hill   = Mesh.From(vertices, indices);    // x, y, z per vertex; three vertex numbers per triangle
Mesh ball   = Mesh.Smooth(rock);               // the same triangles, SMOOTH-shaded — nothing moves
Mesh ball2  = Mesh.Smooth(rock, 30);           // ...sharing a normal only across faces within 30 degrees

Draw.Camera(ex, ey, ez, tx, ty, tz, fovDeg);   // eye position, the point it looks at, vertical field of view
Draw.Light(dx, dy, dz, sun, ambient);          // the direction the sun SHINES ALONG, its colour, the sky colour
Draw.Fog(color, near, far);                    // haze from `near` to fully `color` at `far`, in scene units

Draw.Mesh(crate, x, y, z, color);                                // placed
Draw.Mesh(crate, x, y, z, rx, ry, rz, scale, color);             // rotated (radians) and scaled uniformly
Draw.Mesh(crate, x, y, z, rx, ry, rz, sx, sy, sz, color);        // …or per axis

Draw.Mesh(ground, x, y, z, Textures.Rock);                       // …or one of the app's TEXTURES instead of a colour
Draw.Mesh(ground, x, y, z, Textures.Rock, 0.25);                 // …tiled every four world units
Draw.Mesh(ground, x, y, z, Textures.Rock, 0.25, "#9bba66");      // ...and TINTED: the texture times this colour

Draw.Sprite(Textures.Coin, x, y, z, w, h);                       // a BILLBOARD - always faces the camera
Draw.Sprite(Textures.Sheet, sx, sy, sw, sh, x, y, z, w, h);      // ...a frame out of a sprite SHEET
Draw.Sprite(Textures.Sheet, sx, sy, sw, sh, x, y, z, w, h, "#f80");   // ...tinted
```

## Description    {#description}

### A scene is drawn each frame, like everything else on a canvas   {#a-frame}
The meshes live in fields because building one costs a buffer upload; everything else is said again every frame.
The 2D verbs and the 3D verbs share the frame body: the sky is a `Draw.Rect`, the scene is drawn over it, and the
score is a `Draw.Text` over that — in the order they are written.

```osy title="a tree on a hill, lit from the side" test app=arcade-3d
[Page("/tree")]
[AllowAnonymous]
component Tree() {
  Mesh ground;
  Mesh trunk;
  Mesh crown;
  double t = 0;

  on mount {
    ground = Mesh.Plane(60, 60);
    trunk = Mesh.Cylinder(0.25, 0.35, 1.6, 8);
    crown = Mesh.Cone(1.3, 3.2, 8);
  }

  on frame (double dt) {
    t += dt;
    Draw.Rect(0, 0, 640, 360, "#bfe3ff");                    // the sky, in 2D, BEHIND the scene
    Draw.Camera(0, 3, 12, 0, 1.5, 0, 45);
    Draw.Light(-0.5, -1, -0.4, "#fff4d6", "#9ec5ff");
    Draw.Fog("#bfe3ff", 20, 60);
    Draw.Mesh(ground, 0, 0, 0, "#7bbf5a");
    Draw.Mesh(trunk, 0, 0.8, 0, "#8a5a3c");
    Draw.Mesh(crown, 0, 3.2, 0, 0, t * 0.3, 0, 1, "#3f8f4a");   // slowly turning about y
    Draw.Text("a tree", 12, 12, "#204020", 20);              // the HUD, in 2D, OVER the scene
  }

  render { Canvas(w: 640, h: 360); }
}
```

### Which way is up, where is the origin, and how big is one unit?   {#coordinates}
The scene is **right-handed with y up**: `+x` is screen-right for a camera looking along `−z`, and `+y` is up.
Units are whatever you choose — the builders, the camera and the fog all speak the same ones. Rotations are in
**radians**, like `Math.Sin`, and apply **x, then y, then z** — pitch a bird, then yaw it to its heading.

A mesh's origin is its **centre** (a `Plane` is centred at `y = 0`, a `Cylinder`/`Cone` runs from `−height/2` to
`+height/2`), so a tree of height 3 standing on the ground is drawn at `y = 1.5`.

### The light is a direction, and the second colour is the sky   {#light}
`Draw.Light(dx, dy, dz, sun, ambient)` takes the direction the sun **shines along** — `(−0.5, −1, −0.4)` is a sun
high and to the right, casting shadows down and to the left. Faces turned toward it get `sun`; faces turned away
get the `ambient` colour, stronger on faces that look up (the sky) than on faces that look down. Shadows are
cast by every mesh onto every mesh, softened at the edge, within the range the fog reaches — a shadow far beyond
the fog would never be seen, and the shadow map's resolution is spent where it shows.

### Fog is what makes distance read   {#fog}
`Draw.Fog(color, near, far)` blends every surface toward `color` from `near` (no fog) to `far` (only fog). Give
it the sky's colour and a far hill dissolves into the horizon the way it does outdoors; it also sets how far the
shadow map reaches, so the two are tuned together.

### `Mesh.From` builds anything the builders cannot   {#from}
A heightfield, a bird's body, a rock: give it every vertex as `x, y, z` and every triangle as three vertex
numbers. Faces are **flat-shaded** from their own winding, so wind each triangle **counter-clockwise seen from
the outside** — a face wound the other way is culled as a back face and simply is not there. An index outside
the vertex list, or a list whose length is not a multiple of three, is an error that names the position.

```osy title="a ridge from a heightfield" test app=arcade-3d
[Page("/ridge")]
[AllowAnonymous]
component Ridge() {
  Mesh ridge;

  on mount {
    var verts = new List<double>();
    var idx = new List<int>();
    var cols = 24;
    var rows = 6;
    for (var r = 0; r <= rows; r++) {
      for (var c = 0; c <= cols; c++) {
        var x = (c - cols / 2.0) * 2;
        var z = (r - rows / 2.0) * 2;
        var y = Math.Sin(c * 0.5) * 1.5 + Math.Cos(r * 0.9) * 0.6 + 2;
        verts.Add(x); verts.Add(y); verts.Add(z);
      }
    }
    for (var r = 0; r < rows; r++) {
      for (var c = 0; c < cols; c++) {
        var a = r * (cols + 1) + c;
        var b = a + 1;
        var d = a + cols + 1;
        var e = d + 1;
        idx.Add(a); idx.Add(d); idx.Add(b);   // counter-clockwise seen from above (+y)
        idx.Add(b); idx.Add(d); idx.Add(e);
      }
    }
    ridge = Mesh.From(verts, idx);
  }

  on frame (double dt) {
    Draw.Rect(0, 0, 640, 360, "#cfe8ff");
    Draw.Camera(0, 8, 22, 0, 2, 0, 40);
    Draw.Light(-0.4, -1, -0.6, "#fff3d0", "#a9cbff");
    Draw.Fog("#cfe8ff", 25, 70);
    Draw.Mesh(ridge, 0, 0, 0, "#6faf58");
  }

  render { Canvas(w: 640, h: 360); }
}
```

### Colours are the 2D verbs' colours   {#colours}
`#rgb`, `#rrggbb`, `rgb(…)` and `rgba(…)` — the alpha is ignored, a mesh is opaque. Anything else draws
**magenta**, the colour every renderer uses to mean "this is not a colour", so a typo is loud rather than dark.

### A texture where the colour goes   {#textures}
Put one of the app's own textures ([textures](https://osysharp.com/reference/ui/textures/)) where a mesh's colour would be and it is **projected onto the
surface** — a rock face, a brick wall, a grass floor:

```osy syntax
Draw.Mesh(ground, 0, 0, 0, Textures.Grass, 0.25);   // a tile every four world units
Draw.Mesh(crate, 2, 0.5, 0, Textures.Crate);        // one tile per unit, the default
```

The trailing number is **tiles per world unit**. It is on the draw rather than on the scene because a ground plane
and a crate want different densities in the same frame.

⚠️ **Tiling is per world unit, and getting it too low is the commonest way a texture goes missing.** A hill seven
units across at `0.12` gets less than one tile — the texture is there and stretched until nothing reads. Something
like `0.5` puts three or four across the slope, which is what reads as ground cover.

### One texture, many tones — the tint   {#tint}
A colour after the tiling is a **tint**: the texture is *multiplied* by it, so `"#ffffff"` is the texture untouched
and anything darker or warmer shades it.

```osy syntax
Draw.Mesh(hill, x, y, z, Textures.Grass, 0.5, "#9bba66");   // near: green
Draw.Mesh(hill, x, y, z, Textures.Grass, 0.5, "#c9cf8a");   // far: washed toward the haze
```

⭐ **This is what lets one texture carry a whole scene.** Without a tint every textured surface is the texture's own
colours exactly, so a valley of seventeen hills is seventeen identical hills. With it, the same moss reads as
near-green and far-gold — which is most of what makes a landscape recede.

A `Gradient` is **refused** here rather than quietly ignored: a mesh's colour is one value for the whole surface,
and a canvas fill has nowhere to go in it.

⚠️ **It is projected, not UV-mapped, and the difference is worth knowing.** The texture is a property of the world
*position*: the renderer samples it down the three world axes and blends by the surface normal. That means it works
on every mesh — including one you built with `Mesh.From`, which has no texture coordinates to map with — and it
tiles seamlessly across the joint between two meshes, which is what a floor made of several planes wants.

What it cannot do is a **decal**: there is no way to put a label on one face of a crate, or a face on a character.
For that, draw the mark with the 2D verbs over the scene — they paint on the same canvas, after it.

### Smooth shading, without changing the geometry   {#smooth}
Every mesh is **flat-shaded** by default: each triangle carries one normal, so a sphere reads as facets. That is
the low-poly look, and it is right for a crate or a crystal. For a hill, a cloud, a tree or a character it is what
makes a scene read as *blocky*.

`Mesh.Smooth` averages the shading normals of faces that meet at the same point:

```osy syntax
Mesh hills  = Mesh.Smooth(Mesh.From(vertices, indices));   // rolling, not faceted
Mesh cloud  = Mesh.Smooth(Mesh.Sphere(1, 2));              // a round ball at 320 faces
Mesh trunk  = Mesh.Smooth(Mesh.Cylinder(1, 1, 2, 18));     // round SIDE, flat CAPS - one call
```

**Nothing moves.** The positions, the silhouette and the number of triangles are identical; only what the lighting
is handed changes. So it costs nothing per frame - do it once, where you build the mesh.

* **The angle is what makes one verb safe on any mesh.** Two faces share a normal only if they meet within `60`
degrees, so the same call that rounds a sphere leaves a **box untouched** - every edge of a box is 90 degrees. Pass
your own angle as a second argument when you want more or less: `Mesh.Smooth(m, 100)` will round a box's corners,
and `Mesh.Smooth(m, 20)` keeps all but the gentlest creases sharp.

Because the threshold is per *pair of faces*, one call gives a cylinder a round side and flat caps - the answer you
would otherwise have to build by hand.

**Vertices are welded by position**, to within a hundredth of a millimetre at world scale. Two vertices genuinely
closer together than that are treated as one point.

### Sprites — a picture standing in the world   {#sprites}
A `Draw.Sprite` is a flat rectangle of one of your textures that **always faces the camera**, placed at a world
position and sized in world units:

```osy syntax
Draw.Sprite(Textures.Coin, 4, 1.5, -2, 0.8, 0.8);               // the whole image
Draw.Sprite(Textures.Sparks, 128, 0, 128, 128, x, y, z, s, s);  // one FRAME of a 2x2 sheet
```

The nine-number form takes a **source rectangle in the image's own pixels**, exactly as [Canvas](https://osysharp.com/reference/ui/canvas/)'s
`Draw.Image` does — so one sprite sheet is cut up the same way in 2D and in 3D, and an animation is just picking
`sx` from a frame counter.

⭐ **This is what a textured mesh cannot do.** A mesh's texture is projected by world *position*; a sprite carries
the picture itself, so a specific image lands on it wherever it stands. Particles, pickups, distant trees, motes of
pollen and world-space markers are all sprites.

**They behave correctly against the scene**, which is most of the work:

| | |
|---|---|
| **hidden by what is in front** | a sprite behind a hill is not drawn — it is depth-tested like everything else |
| **transparent** | the image's alpha is respected, so a soft-edged mote is a mote and not a square |
| **drawn in the right order** | overlapping sprites blend back-to-front, and none of them hides another |
| **fogged** | a distant sprite fades into the haze with the rest of the scene |

**A sprite is unlit**, and that is deliberate: it is a painted image — a flame, a coin, a marker — and shading it
by a surface normal it does not have would only mean fighting the sun. A trailing colour **tints** it (multiplied,
so `"#ffffff"` is the image untouched), which is how one flame sheet serves an orange flame and a blue one.

### It lowers to a kernel like the rest of the frame body   {#kernel}
Every 3D verb and every builder is part of the host contract a frame body is lowered against, so a body that uses
them still runs as a compiled JavaScript kernel — the hot loop of a game pays no interpreter cost for being 3D.
A mesh in a field is a plain value to the kernel; build it in `on mount`, read it in `on frame`.

### Where WebGL is missing, the scene is missing   {#degrade}
The scene is rendered by the browser's GPU. In an environment with no WebGL the 3D verbs log one error and draw
nothing, while the 2D verbs keep working — so a HUD and a backdrop still appear over a blank scene rather than
the page failing. Real browsers all have it; the headless DOM the unit tests run in does not, which is why the
pixels are proven by the visual harness and not by a unit test.

### What a frame can afford   {#budget}
The cost is per **mesh drawn** and per **face** — not per pixel — plus one shadow pass over the same meshes. A few
hundred `Draw.Mesh` calls over meshes of tens to low hundreds of faces each is comfortably 60fps; a `Sphere` at
detail 4 is 5,120 faces and is the wrong choice for anything smaller than a planet.

## See also {#see-also}
- [Canvas](https://osysharp.com/reference/ui/canvas/) — the surface itself and the 2D verbs, which draw under and over the scene
- [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) — `on frame (double dt)`, the clock that drives it
- [component](https://osysharp.com/reference/ui/component/) — component state, where the meshes and the world live


---

<!-- https://osysharp.com/reference/ui/cell-template/ -->

# Cell template (your own content in a control's cell)

> A control that paints cells — a grid — writes plain text in each one. A `slot <Field> { row => … }` block on the call replaces that column's cell with your own content: a status pill, an avatar, a link, a button. The control keeps the columns, the sorting and the responsive layout; you decide what a cell looks like.

<!-- id: ui-cell-template · area: ui · stability: stable · html: https://osysharp.com/reference/ui/cell-template/ -->

## Summary        {#summary}
A grid renders each cell as text. That is right for a name or a price, and wrong for a **status** — which wants a
coloured pill — or a person, which wants an avatar.

A **cell template** is how you take a column over. Inside the control's call, write `slot <Field> { row => … }` and
that column's cells render *your* content instead of text. Everything else about the grid still works: the column
still sorts, the table still becomes cards on a narrow screen, the row still raises its selection event.

## Signature      {#signature}
```osy syntax
DataGrid(rows: <list>, columns: [ … ]) {
  slot <Field> { <row> => <content> }     // <Field> is a property of the row; <row> binds that row
}
```

`<Field>` names a property of the row type. If it names something that is not a property — a typo, a column you
renamed — that is a **compile error**, not a cell that quietly renders nothing.

## Description    {#description}

### A status pill   {#pill}
The classic case: the status column should be a pill, tinted by what the status *is*.

```osy title="status as a pill" test app=ui-cell-template
enum OrgStatus {
  Active,
  [Label("Suspended")] Suspended,
}

entity Organization {
  [Required, MaxLength(100)] string Name;
  OrgStatus Status = OrgStatus.Active;
}

class GridColumn { public string Key; public string Label; }

control DataGrid<T> {
  contractVersion "1.0"
  participation headless
  props {
    T[] rows;
    GridColumn[] columns;
  }
  events { rowSelected(T row); }
}

theme Admin {
  Colors {
    Surface1      = "#fbfbfa";
    TextSecondary = "#5f5f5a";
    BgSuccess     = "#eaf3de";
    TextSuccess   = "#3b6d11";
    BgWarning     = "#faeeda";
    TextWarning   = "#854f0b";
  }
  Radius { Pill = "999px"; }
}

enum Tone { Neutral, Success, Warning }

/// The pill itself — an ordinary component. Its label is CONTENT, so it can hold an enum-typed field, which renders
/// as that member's label.
[Composable] component Badge(Tone tone) {
  variants {
    base { Display = Display.InlineFlex; Px = "10px"; Rounded = Radius.Pill; }
    tone {
      Neutral { Bg = Colors.Surface1;  Color = Colors.TextSecondary; }
      Success { Bg = Colors.BgSuccess; Color = Colors.TextSuccess; }
      Warning { Bg = Colors.BgWarning; Color = Colors.TextWarning; }
    }
  }
  render { Row(align: Align.Center) { Slot; } }
}

[Page("/orgs")]
[Render(CSR)]
component OrganizationsPage() {
  var orgs = Organization.ToList();

  render {
    DataGrid(rows: orgs, columns: [
      new GridColumn { Key = "Name", Label = "Organization" },
      new GridColumn { Key = "Status", Label = "Status" }
    ]) {
      slot Status { o =>
        Row(align: Align.Center) {
          if (o.Status == OrgStatus.Active) { Badge(Tone.Success) { Text(o.Status); } }
          else { Badge(Tone.Warning) { Text(o.Status); } }
        }
      }
    }
  }
}
```

Three things are worth naming, because each is a decision you are making:

- **The pill is yours.** It is an ordinary component, not something the grid provides. Restyle it, or replace it with
  something else entirely, and no control has to know.
- **The colour is a UI decision, so it lives in the UI.** `Active` is green because *this screen* says so. Nothing
  about the colour belongs on the enum — an enum is a set of values, not a palette.
- **`Text(o.Status)` shows the member's label**, because it reads an enum-typed field directly. See [[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/).

### What the `{ o => … }` parameter binds to   {#row}
`{ o => … }` binds that row, typed as the row's entity — so `o.Status`, `o.Name` and any other property type-check.
You can read whatever the row has, not just the column you are rendering:

```osy syntax
slot Name { o => Row(gap: 2) { Avatar(o.AvatarUrl); Text(o.Name); } }
```

### Columns you do not template are unchanged   {#untouched}
Only the columns you write a `slot` for change. Everything else keeps rendering as text, with the alignment and
formatting the column declared. Start by templating the one column that needs it.

## See also       {#see-also}
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the controls a cell template applies to, and how a control is declared
- [[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/) — why `Text(o.Status)` reads as "Suspended" and not a stored number
- [Slot (child content)](https://osysharp.com/reference/ui/slots/) — the `slot` construct in its other role: filling a component's named slot


---

<!-- https://osysharp.com/reference/ui/clipboard/ -->

# Clipboard

> Put a string on the visitor's system clipboard. One verb, callable from any action, so a copy button — an API key, a share link, a code snippet, an invoice number — is ordinary app code. The platform ships the mechanism and renders none of the chrome.

<!-- id: ui-clipboard · area: ui · stability: stable · html: https://osysharp.com/reference/ui/clipboard/ -->

## Summary        {#summary}
`Clipboard.Copy(text)` writes a string to the visitor's system clipboard. It is available in every component, and it
is the whole surface: one verb, one argument.

It exists because "copy this" is a browser act with no server equivalent — the clipboard belongs to the person at the
keyboard, not to your app or to the machine your app runs on. So it is a verb, alongside [Navigation](https://osysharp.com/reference/ui/navigation/)'s and
[theme tokens](https://osysharp.com/reference/ui/theming/)'s, rather than a function you could call from a server body.

The platform ships **no copy button**. Where it lives, what it looks like, and whether it says "Copied!" afterwards
are your layout's decisions.

## Signature      {#signature}
```osy syntax
Clipboard.Copy(text)     // put `text` on the system clipboard; returns nothing
```

## Description    {#description}

### Copying text — where the call goes   {#calling}
It is an ordinary statement in an action body:

```osy title="a copy button" test app=drop-ship-order
component ShareLink(string Url) {
  action Copy() { Clipboard.Copy(Url); }

  render {
    Row {
      Text(Url);
      Button("Copy", onPress: Copy);
    }
  }
}
```

### Showing a "Copied!" — the feedback is yours   {#feedback}
The verb returns nothing, so the "Copied!" is yours to render — which is what you want, because the wording, the
placement and how long it lingers are design decisions. Set your own state on the click:

```osy title="Copied!, for a moment" test app=drop-ship-order
component CopyKey(string Key) {
  bool copied = false;

  action Copy() {
    Clipboard.Copy(Key);
    copied = true;
  }

  render {
    Button(copied ? "Copied!" : "Copy key", onPress: Copy);
  }
}
```

### The text goes across verbatim    {#verbatim}
Whatever you pass is what lands on the clipboard, byte for byte. Nothing is escaped, trimmed, or rewritten — a code
snippet containing `<script>` arrives as those nine characters, because a clipboard is not a rendering surface and
"helpfully" sanitizing it would corrupt the very thing someone asked to copy.

That also means **you** decide what is copyable. Copying a value your visitor cannot see is not a boundary the
clipboard enforces; the ordinary rules about what a component may read still apply, and they apply here unchanged.

### When it cannot copy    {#failures}
A browser may refuse a clipboard write — most often because the page is served over plain `http` from an address that
is not `localhost`, where the modern clipboard API does not exist at all. The platform falls back to the older
selection-based copy, which works there, so a copy button on an internal http-only deployment still functions.

If both paths fail, **nothing is copied and the browser console says so**, naming the reason. The action itself
carries on: a refused copy never throws and never aborts the rest of your body.

### Can I READ the clipboard? No   {#no-read}
There is no `Clipboard.Read`. Reading someone's clipboard is a permission-gated act with a prompt attached, and
nothing in the platform needs it — a paste arrives through an ordinary `Input` the moment the visitor presses the
keys. If you have a case that genuinely needs it, that is a conversation to have rather than a gap to work around.

### Can I name something `Clipboard`? — shadowing   {#shadowing}
`Clipboard` is an ambient name, so an identifier of your own wins — a parameter, a state member or a variable called
`Clipboard` shadows it, exactly as it does for [Navigation](https://osysharp.com/reference/ui/navigation/) and [theme tokens](https://osysharp.com/reference/ui/theming/). Nothing you already named breaks
because this verb exists.

### Copying from rendered markdown    {#markdown}
A `Markdown(...)` document renders inside the atom, so your app cannot reach the code blocks inside it to hang a
button on them. That case has its own opt-in — see [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) `#code-actions`, which renders a copy control per
code block using an icon and tooltip you supply.

## See also {#see-also}
- [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) — per-code-block copy inside a rendered document
- [Navigation](https://osysharp.com/reference/ui/navigation/) — the other browser verbs (`Go`, `Open`, `Close`)
- [theme tokens](https://osysharp.com/reference/ui/theming/) — `Theme.Toggle()` / `Theme.Set(mode)`, the same shape
- [component](https://osysharp.com/reference/ui/component/) — where actions live


---

<!-- https://osysharp.com/reference/ui/connection/ -->

# Connection

> The live state of the browser's link to the server, and the two verbs that recover it. A component you nominate as your app's connection-loss surface reads `Connection.State` to switch between "reconnecting" and "lost", shows `Connection.Attempts`, and calls `Connection.Retry()` / `Connection.Reload()` from its buttons. The platform detects the drop and mounts your surface; it renders none of the chrome.

<!-- id: ui-connection · area: ui · stability: stable · html: https://osysharp.com/reference/ui/connection/ -->

## Summary        {#summary}
`Connection` is the live state of the browser's link to the server. When the server drops mid-session — it restarted,
the network went away, a deploy is rolling — the platform detects it, keeps the current page intact, and mounts the
component you nominated as your **connection-loss surface** (see [UI surfaces (app.Ui)](https://osysharp.com/reference/config/ui/)). That surface reads `Connection` to tell
the user what happened and offer a way back.

It exists so you can write that surface — a full-screen "can't reach the server" curtain, a quiet "reconnecting…" strip,
a branded card — in your own design. The platform ships a plain default overlay if you nominate none; the moment you do,
`Connection` is what your surface binds to.

`Connection` is available **only** inside your nominated connection-loss surface. An ordinary page never reads it (there
is nothing for it to say there) — reading it elsewhere is an unbound-name error.

## Signature      {#signature}
```osy syntax
Connection.State       // where the link stands right now — a ConnState (Ok / Reconnecting / Lost)
Connection.Attempts    // how many reconnect attempts have been made since the drop (an int)

Connection.Retry()     // try the server now — clears the surface if it answers
Connection.Reload()    // reload the page
```

`Connection.State` is a **`ConnState`** — a three-member enum:

| Member | Meaning |
|---|---|
| `ConnState.Ok` | The server is reachable. Your surface is unmounted, so you never render this case. |
| `ConnState.Reconnecting` | The link just dropped; the platform is retrying with backoff. A soft, transient state. |
| `ConnState.Lost` | Several retries failed — the server looks genuinely gone. Offer Retry / Reload. |

## Description    {#description}

### How a drop is detected    {#detection}
Every request the page makes reports up or down. A request that can't reach the server at all, or that comes back as a
gateway error (the server is down or restarting), flips `Connection.State` to `Reconnecting`. The platform then pings
the server on a backoff, escalating to `Lost` after a few failed tries, and returns to `Ok` the instant anything
succeeds. A normal error response — a `401`, a `404`, a `500` — is **not** a connection loss: the server answered, so
the link is fine, and your ordinary error handling still runs.

### Does my surface re-render as the state moves?   {#reactivity}
Reading `Connection` in your surface's `render` subscribes it: when the state moves `Reconnecting → Lost → Ok`, or the
attempt count climbs, the surface re-renders. A `switch` on `Connection.State` re-runs on every transition with no
polling.

### Your surface must render with the server gone   {#offline}
Your connection-loss surface has to render **with the server gone**, so the platform fetches it up front — while the
link is still alive — and holds it ready. That puts one real constraint on the component: it must be **self-contained**.
Build it from the [component](https://osysharp.com/reference/ui/component/) built-in elements only — no child components to fetch, no data to load — because
anything it would fetch when it mounts is exactly what's unreachable. It renders from `Connection` and nothing else.

### Retrying by hand — `Connection.Retry()`   {#recovering}
`Connection.Retry()` pings the server immediately — the button a user presses when they think the network is back. If
the server answers, the surface clears itself; if not, the state stays `Lost` and the backoff continues.
`Connection.Reload()` reloads the page outright — the heavier reset for when a retry isn't enough.

Neither is required: a surface can simply say "reconnecting…" and let the automatic backoff recover on its own. The
verbs are there for the `Lost` case, where the user wants a button.

### Can I name something `Connection`? — shadowing   {#shadowing}
`Connection` is an ambient name, not a keyword. A parameter or state member named `Connection` shadows it, exactly as a
local variable shadows any other ambient. Nothing is reserved.

## Examples       {#examples}

### A connection-loss surface    {#example-surface}
One component covers both states — a quiet strip while reconnecting, a blocking card once lost. Nominate it with
[UI surfaces (app.Ui)](https://osysharp.com/reference/config/ui/) (`app.Ui = new AppUi { ConnectionSurface = OfflineOverlay };`).

```osy title="a connection-loss surface" test app=ui-connection
// The tokens this overlay draws with. They are YOUR app's — the platform declares none of them — so a copy of this
// example needs a theme that names them (or your own names substituted throughout).
theme App {
  Colors {
    Surface0 = "#FFFFFF"; Surface1 = "#F7F8FA";
    Border = "#E3E6EA"; BorderStrong = "#C7CCD3";
    FillAccent = "#0077B6"; OnAccent = "#FFFFFF"; TextMuted = "#6B7280";
  }
  Radius { Card = "12px"; Control = "8px"; }
  FontWeight { Medium = 500; }
}

[AllowAnonymous]
component OfflineOverlay() {
  action Retry()  { Connection.Retry(); }
  action Reload() { Connection.Reload(); }

  render {
    if (Connection.State == ConnState.Lost) {
      // A full-screen curtain: the server looks gone, so block the dead view and offer a way back.
      Row(align: Align.Center, justify: Justify.Center, position: Position.Fixed, inset: 0, bg: Colors.Surface0) {
        Box(bg: Colors.Surface1, border: Colors.Border, borderW: 1, rounded: Radius.Card, p: 5, maxW: "400px") {
          Stack(gap: 3, align: Align.Center) {
            Text("Can't reach the server", fontWeight: FontWeight.Medium);
            Text("Your connection dropped. Retry, or reload the page.", color: Colors.TextMuted, textAlign: TextAlign.Center);
            Row(gap: 2, w: "100%") {
              Pressable(onClick: Retry,  grow: 1) { Row(align: Align.Center, justify: Justify.Center, h: "40px", bg: Colors.FillAccent, color: Colors.OnAccent, rounded: Radius.Control) { Text("Retry"); } }
              Pressable(onClick: Reload, grow: 1) { Row(align: Align.Center, justify: Justify.Center, h: "40px", borderW: 1, border: Colors.BorderStrong, rounded: Radius.Control) { Text("Reload"); } }
            }
          }
        }
      }
    } else if (Connection.State == ConnState.Reconnecting) {
      // A quiet, non-blocking top strip — a transient blip should not lock the UI.
      Row(align: Align.Center, justify: Justify.Center, position: Position.Fixed, top: 0, left: 0, right: 0, py: 2, bg: Colors.Surface1, borderBW: 1, border: Colors.Border) {
        Text("Reconnecting…", fontWeight: FontWeight.Medium);
      }
    }
  }
}
```

### Showing the attempt count    {#example-attempts}
```osy syntax
Text("Reconnecting… (attempt " + Connection.Attempts + ")");
```

## See also       {#see-also}
- [UI surfaces (app.Ui)](https://osysharp.com/reference/config/ui/) — `app.Ui`, where you nominate the component `Connection` binds to.
- [component](https://osysharp.com/reference/ui/component/) — components, `render` blocks, actions, and the built-in elements a surface is built from.
- [routes and pages](https://osysharp.com/reference/ui/routing/) — how a failed navigation rolls back and leaves the current page intact underneath the surface.


---

<!-- https://osysharp.com/reference/ui/dialogs/ -->

# Dialog.Open / Dialog.Ask / Dialog.Confirm / Dialog.Discard

> Opens a component in an overlay above the current screen, with a unit of work you choose: `Inherit` makes its edits a savepoint of the work already open, so Confirm hands them to the surrounding page and that page's Save decides; `Root` gives the dialog its own, so its Confirm is the save. `Dialog.Ask<TEnum>` waits and returns the answer the dialog supplies with `Dialog.Confirm(value)`.

<!-- id: ui-dialogs · area: ui · stability: stable · html: https://osysharp.com/reference/ui/dialogs/ -->

## Summary        {#summary}
A **dialog** is an ordinary component shown in an overlay above the current screen. What makes it a dialog is not how
it looks — you draw that yourself — but the **unit of work** it runs in, which you choose when you open it:

- **`unitOfWork: Inherit`** — the dialog's edits are a *savepoint* of whatever unit of work is already open. Confirm
  hands them to that unit of work: the page goes **dirty**, and **nothing is saved** until the page's own Save.
- **`unitOfWork: Root`** — the dialog owns its unit of work. There is nothing to hand its changes to, so its Confirm
  **is** the save.

`Dialog.Open` shows a dialog and moves on. `Dialog.Ask<TEnum>` shows one and **waits for an answer**.

## Signature      {#signature}
```osy syntax
Dialog.Open(SomeComponent(prop: value), unitOfWork: Inherit);   // show it; don't wait
var answer = Dialog.Ask<Decision>(SomeComponent(…), unitOfWork: Root);   // show it; wait for the answer

Dialog.Confirm();          // inside the dialog: accept, and close
Dialog.Confirm(value);     // …and hand `value` back to whoever called Dialog.Ask
Dialog.Discard();          // inside the dialog: throw its changes away, and close
```

## Description    {#description}

### Choosing the unit of work — the one decision {#unit-of-work}

`unitOfWork:` is **required and has no default**. That is deliberate: the wrong choice loses work *silently*, and
nothing about a component says which it should be.

| | `Inherit` | `Root` |
|---|---|---|
| whose work does it edit? | the surrounding page's | its own |
| what does Confirm do? | promotes into the page's unit of work | commits |
| after Confirm, is it saved? | **no** — the page is dirty; its Save decides | **yes** |
| what does Discard do? | drops the savepoint; the page never knew | drops everything the dialog did |
| when to reach for it | editing something the page is already about | something unrelated to the page — account settings, preferences |

Two examples of the difference, in words. A form about a project, with a button that opens a dialog editing that
project's data: **Inherit**, so pressing OK marks the project dirty and the page's Save is still what persists it. The
avatar in a nav rail opening general settings that have nothing to do with the screen behind: **Root**, so Save in the
dialog saves, and closing the page never had anything to do with it.

⚑ **"Can a `Root` dialog edit a row the PAGE loaded?" — yes, and this is the question people actually ask.** The
table above answers by intent; the mechanical worry underneath it is whether the two units of work fight over the
same row. They do not. An entity is one row, and a `Root` dialog handed one edits it and commits it on `Confirm` —
the page's own queries then read the committed value back like any other change.

So both choices WORK for an edit, and the difference is when it is saved:

| | `Root` | `Inherit` |
|---|---|---|
| pressing OK | saves, then and there | marks the page dirty; the page's Save persists it |
| reach for it when | the press IS the act — "Lend", "Rename", a switch in a settings sheet | the page has its own Save and this edit belongs to it |

⚠ **A page with no Save button and a dialog on `Inherit` is the shape that loses work**: OK promotes the edit into
a unit of work nothing ever commits, and the change is gone at the next navigation with no error anywhere. If the
page does not save, the dialog must.

### A dialog opened from a dialog {#nesting}

`Inherit` means *whatever unit of work is currently open* — not "the page". So a dialog opened from inside another
dialog savepoints off **that** dialog, and its changes ride the outer one's Confirm. Closing the outer dialog without
confirming discards the inner one's work too, which is what you want: the user never left the sheet they were in.

### Waiting for an answer — `Dialog.Ask<TEnum>`   {#ask}

`Dialog.Ask<TEnum>` shows a dialog and **waits**. The dialog answers with `Dialog.Confirm(value)`, and that value is
what `Ask` evaluates to.

The type argument is an **enum you declare**. A dialog asks a question, and a question has a known set of answers —
which is exactly what an enum is. It also means the caller's `== Decision.Delete` is checked when you compile rather
than compared against a string nobody declared. A dialog dismissed with `Dialog.Discard()` answers nothing, so treat
"no answer" as its own case.

`Ask` works from a **server function** too, which is what makes a per-row question possible: a function can read rows,
loop over them, ask about each one, and carry on with the answers. The run suspends at each question, the browser
shows the dialog, and the run resumes where it left off with its data intact. The flow must have been **started by
someone using the app** — a scheduled or workflow-driven run has nobody to ask.

### What the platform draws — nothing {#look}

The platform contributes exactly two things: a host element that is already an overlay above the page, and the unit
of work. It draws **no** backdrop, panel, buttons or animation.

That is why a dialog can look like anything — a small confirmation, or a full settings surface with its own nav rail.
Your component owns the dim, the panel, its width, its corners and its shadow, exactly as it owns any other screen.
You do **not** need to position it: being above the page is the platform's job.

#### Two things you owe it, and neither is visual {#owed}

Drawing it yourself means you also own what makes it a *dialog* rather than a box that happens to float. Both of
these are invisible on screen and both fail loudly later, so they are the first two lines of the panel, not a polish
pass:

```osy title="a panel you draw yourself" test app=ui-dialog-own-panel
entity Locomotive { [MaxLength(80)] string Name; bool Retired; }

[AllowAnonymous]                                    // ① or the fetch 403s — see below
[Render(CSR)]
component ConfirmRetire(Locomotive engine) {
  // ③ a handler is an ACTION of this component, passed by NAME. `onPress: Dialog.Discard`
  //    reads perfectly and does not compile — see below.
  action Cancel() { Dialog.Discard(); }
  action Retire() { engine.Retired = true; Dialog.Confirm(); }

  render {
    Stack(role: UiRole.Dialog, label: "Retire this engine?") {  // ② or no test can reach inside it
      Text("Retire this engine?", fontSize: FontSize.Heading);
      Row(gap: 2, justify: Justify.End) {
        Button("Cancel", onPress: Cancel);
        Button("Retire", onPress: Retire, tone: Tone.Danger);
      }
    }
  }
}

[Page("/engines")]
[AllowAnonymous]
[Render(CSR)]
component Engines() {
  var engines = Locomotive.ToList();
  action Ask(Locomotive e) { Dialog.Open(ConfirmRetire(engine: e), unitOfWork: Root); }
  render { Stack(gap: 2) { foreach (var e in engines) { Button(e.Name, onPress: () => Ask(e)); } } }
}
```

**① `[AllowAnonymous]` on the component itself, in an app with no sign-in.** `Dialog.Open` fetches its component as
its own entry point, so the page's marker does not cover it. Without one the fetch answers **403** and the button
appears to do nothing — there is no error on screen, because the failure is in a request the page made on your
behalf. The lint `ui-dialog-target-unreachable` now names this before you run.

**② `role: UiRole.Dialog` and `label:` on the panel.** The role is what an assistive reader announces, and the label
is the container's NAME — which is what `within:` selects. Omit them and `Assert.Dialog(…)` reports *"a dialog IS
open … but nothing in it carries an accessible name"*, and every `Ui.Click("Save", within: "…")` inside it has
nothing to match.

**③ `onPress:` takes an ACTION of this component, by name.** `onPress: Dialog.Discard` is the spelling everyone
reaches for and it does not compile — `Dialog` is also a member of the `UiRole` enum, so the refusal you get talks
about `UiRole.Dialog`, which is not the problem and cannot be the fix. Give the dialog a one-line action and pass
that: `action Cancel() { Dialog.Discard(); }` … `onPress: Cancel`.

> ⛔ **This page taught the broken spelling until 2026-09-01, in a `syntax` fence the compile gate does not check.**
> Eval run 16 copied it into FOUR dialogs and spent fourteen calls — a quarter of the run — hunting a `Dialog`
> namespace that does not exist. The fence above is COMPILED now, so it cannot say that again.

> ⚑ **Measured, and this section exists because the page caused it.** Two eval runs read this page, drew their own
> panel exactly as it invited, and shipped the same defect — one of them three times in one file, then spent eight
> consecutive lookups discovering the 403. A third run skipped the page, ran `osy kit Dialog`, used the kit control,
> and its first `osy check` passed. The kit's source carries both facts; this page did not, so the runs that did the
> diligent thing were the ones that got it wrong. **If you do not want to own these two, use the kit control**
> ([below](#kit-control)) — it sets both for you.

### The kit's `Dialog` control is a different thing {#kit-control}

The bundled UI kit ships a control also called `Dialog` — `Dialog(title, onDismiss, subtitle)`, `osy kit Dialog`. It
is **not** what this page documents, and the two are not layers of one mechanism:

| | `Dialog.Open(…)` — this page | the kit's `Dialog(…)` control |
|---|---|---|
| what it is | an effect that opens a component in the platform's overlay | markup — a `Scrim` and a panel, drawn in the page's own tree |
| how it appears | you call it | you render it conditionally: `if (confirming) { Dialog("Delete?", onDismiss: Cancel) { … } }` |
| unit of work | its own, and you choose it: `Inherit` or `Root` | **none** — being `[Composable]` it has no mount of its own, so there is nothing to savepoint. Whatever you edit inside it is edited in the **page's** unit of work |
| how it closes | `Dialog.Confirm()` / `Dialog.Discard()` | the `onDismiss` action you passed, which flips your own flag |

So the kit control is a panel that looks like a dialog; it does not give you the scope this page is about. If the
edits inside it must be discardable, or must not touch the page until they are confirmed, open a component with
`Dialog.Open` and choose its unit of work — a kit `Dialog` around the same fields cannot do it, open or closed.

### Saving a Root dialog {#root-save}

A `Root` dialog's Confirm is the only thing that can persist it. A root dialog that edits data and has no reachable
`Dialog.Confirm()` can therefore only ever be discarded — every edit thrown away, with nothing reporting it. `osy
lint` reports that shape as `data-root-dialog-cannot-confirm`; either give it a Confirm, or open it with `Inherit` so
its edits ride the surrounding page's save.

## Examples       {#examples}

An **Inherit** dialog editing a row the page is about. `OK` makes the *page* dirty; the page's own Save is what
persists it:

```osy title="edit-in-a-dialog" test app=ui-dialog-inherit
entity Project { [MaxLength(80)] string Name; }

[Render(CSR)]
component EditProject(Project project) {
  action Ok()     { Dialog.Confirm(); }
  action Cancel() { Dialog.Discard(); }
  render {
    Stack(gap: 2) {
      Input(value: project.Name, placeholder: "Project name");
      Button("Cancel", onPress: Cancel);
      Button("OK", onPress: Ok);
    }
  }
}

[Page("/projects")]
[Render(CSR)]
component Projects() {
  var projects = Project.ToList();
  action Edit(Project p) { Dialog.Open(EditProject(project: p), unitOfWork: Inherit); }
  action Save() { UnitOfWork.Commit(); }
  render {
    Stack(gap: 2) {
      foreach (var p in projects) { Button(p.Name, onPress: () => Edit(p)); }
      Button("Save", onPress: Save);
    }
  }
}
```

**Asking a question** and acting on the answer. The answer type is an enum you declare:

```osy title="ask-before-deleting" test app=ui-dialog-ask
enum Decision { Delete, Keep }

entity Note { [MaxLength(80)] string Title; }

[Render(CSR)]
component ConfirmDelete(Note note) {
  action Yes() { Dialog.Confirm(Decision.Delete); }
  action No()  { Dialog.Confirm(Decision.Keep); }
  render {
    Stack(gap: 2) {
      Text("Delete " + note.Title + "?");
      Button("Keep it", onPress: No);
      Button("Delete", onPress: Yes);
    }
  }
}

[Page("/notes")]
[Render(CSR)]
component Notes() {
  var notes = Note.ToList();
  action Remove(Note n) {
    if (Dialog.Ask<Decision>(ConfirmDelete(note: n), unitOfWork: Inherit) == Decision.Delete) {
      n.Delete();
      UnitOfWork.Commit();
    }
  }
  render {
    Stack(gap: 2) { foreach (var n in notes) { Button(n.Title, onPress: () => Remove(n)); } }
  }
}
```

A **Root** dialog — settings that have nothing to do with the page behind, so they own their unit of work and their
Save really saves:

```osy title="settings-in-its-own-unit-of-work" test app=ui-dialog-root
entity Preferences { [MaxLength(40)] string DisplayName; }

[Render(CSR)]
component Settings(Preferences prefs) {
  action Save()  { Dialog.Confirm(); }
  action Close() { Dialog.Discard(); }
  render {
    Stack(gap: 2) {
      Input(value: prefs.DisplayName, placeholder: "How you appear");
      Button("Close", onPress: Close);
      Button("Save", onPress: Save);
    }
  }
}

[Page("/settings")]
[Render(CSR)]
component SettingsPage() {
  var prefs = Preferences.FirstOrDefault();
  action Open() { Dialog.Open(Settings(prefs: prefs), unitOfWork: Root); }
  render { Stack(gap: 2) { Button("Settings…", onPress: Open); } }
}
```

Drawing the look. The platform positions the overlay; everything you can see is yours:

```osy title="the platform positions the overlay; the panel look is yours" syntax
render {
  Row(align: Align.Center, justify: Justify.Center, w: "100%", h: "100%", bg: Colors.Scrim) {
    Stack(gap: 4, w: "420px", p: 5, bg: Colors.Surface, rounded: Radius.Lg, borderW: 1, border: Colors.Border, shadow: Shadow.Deep) {
      // …your dialog…
    }
  }
}
```

## See also       {#see-also}
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — the unit of work a dialog savepoints, and what `UnitOfWork.Commit()` / `Discard()` do
- [component](https://osysharp.com/reference/ui/component/) — a dialog is an ordinary component; this is what one is
- [routes and pages](https://osysharp.com/reference/ui/routing/) — the other way to hold work in progress: a route, with its own unit of work
- [style props](https://osysharp.com/reference/ui/styling/) — the style props the panel above uses
- `osy kit Dialog` — the kit's same-named CONTROL, which draws a panel and carries no unit of work (see [above](#kit-control))


---

<!-- https://osysharp.com/reference/ui/shell-focused/ -->

# FocusedShell

> An app shell for completing a single task — a checkout, an approval, a configuration step, a guided flow. It is built around three things a task needs: a clear primary action, your position in a sequence, and a way out. It reads the app's own Nav as an ordered flow, so switching to it declares nothing new.

<!-- id: ui-shell-focused · area: ui · stability: stable · html: https://osysharp.com/reference/ui/shell-focused/ -->

## Summary        {#summary}

`FocusedShell` is the arrangement for **completing one thing**: a checkout, an approval, a data-entry or
configuration step, a guided multi-step flow.

⛔ **It is task-oriented, which is not the same as minimal.** A later `Immersive` shell is the experience-oriented
one — a map, a photo editor, a drawing tool, where the content *is* the point. Build this shell by subtraction and
the two become one shell wearing two names. What defines it is what a task needs: **a clear primary action**, a
**position in a sequence**, and a **way out**. The sparse navigation is a *consequence* of those, not the design.

```osy title="a checkout, which is the shape this arrangement is for" test app=ui-shell-focused
[Layout]
[AllowAnonymous]
component FocusedLayout() {
  action SaveDraft() { }

  render {
    FocusedShell(new AppChrome {
      Product = "Expensely",
      Tagline = "New expense claim",
      Home = "/",                       // the way out
      User = new ShellUser { Name = "Olivia Rhye", Initials = "OR" },
      Nav = [
        new NavItem { Label = "Details", To = "/f", Icon = Icons.File, Exact = true },
        new NavItem { Label = "Receipts", To = "/f/receipts", Icon = Icons.Plus, Children = [
          new NavItem { Label = "Upload", To = "/f/receipts/upload", Icon = Icons.Plus },
        ] },
        new NavItem { Label = "Review", To = "/f/review", Icon = Icons.CheckCircle },
      ],
    }) {
      Outlet(retain: 8);
      slot actions { Button("Save draft", onPress: SaveDraft, tone: Tone.Ghost, size: Size.Sm); }
      slot aside {
        ShellAside("Claim summary") {
          Text("Total", fontWeight: FontWeight.Semibold);
          Text("EUR 1,240.00", fontSize: FontSize.Heading);
        }
      }
    }
  }
}

[Page("/")] [Layout(FocusedLayout)] [Title("Home")] [Render(CSR)] [AllowAnonymous]
component FHome() { render { PageHead("Home"); } }

[Page("/f")] [Layout(FocusedLayout)] [Title("Details")] [Render(CSR)] [AllowAnonymous]
component FDetails() { render { PageHead("Claim details"); Card("Basics") { Text("A form."); } } }

[Page("/f/receipts")] [Layout(FocusedLayout)] [Title("Receipts")] [Render(CSR)] [AllowAnonymous]
component FReceipts() { render { PageHead("Receipts"); } }

[Page("/f/receipts/upload")] [Layout(FocusedLayout)] [Title("Upload")] [Render(CSR)] [AllowAnonymous]
component FUpload() { render { PageHead("Upload a receipt"); } }

[Page("/f/review")] [Layout(FocusedLayout)] [Title("Review")] [Render(CSR)] [AllowAnonymous]
component FReview() { render { PageHead("Review and submit"); } }
```

## Signature      {#signature}

```osy syntax
FocusedShell(AppChrome chrome) {
  Outlet(retain: 8);          // the routed page — a CENTRED column with a readable maximum
  slot search   { … }         // the bar, on a pointer. A reader wants this; a checkout will not fill it
  slot actions  { … }         // the bar — "Save draft" is the wizard-shaped one
  slot railFoot { … }         // no rail: under the page, inside the same centred column
  slot aside    { … }         // the SUMMARY column — beside the page at 1100+, under it below that
  slot primary  { … }         // THE COMMIT BAR — `ShellTaskBar`. In the task's own column, under the form
}

class FlowStep {              // what `AppChrome.Steps(current)` projects `Primary()` into
  NavItem Item;  int Number;  bool Done;  bool Active;
}
```

## Description    {#description}

### How does the reader finish?   {#primary}

`slot primary`, and it is the feature that makes this shell task-shaped. Fill it with `ShellTaskBar`, which pins a
commit bar to the foot of the task **inside the page's own column**, so the button that finishes the form sits
directly under the form.

```osy title="the commit bar a task ends with" syntax
slot primary {
  ShellTaskBar {
    Button("Back", onPress: Back, tone: Tone.Ghost);
    Button("Continue", onPress: Next, tone: Tone.Primary);   // last, and the only Primary one
  }
}
```

⚠ **A task you cannot see how to finish is not focused, it is undecorated.** This is the line between this shell
and the experience-oriented one, so if you fork it, keep it.

⚑ **The slot exists on all four arrangements**, so switching never *drops* an app's primary action — the record's
promise is that you lose the layout and nothing else. It is only this shell that is built around it.

### Where does this shell actually go? A ROUTE, not an application   {#where}

⛔ **No application is a four-step wizard.** Reach for `FocusedShell` for **one route group inside a larger app** —
create-claim inside an expense tool, checkout inside a storefront, onboarding inside a product. You enter the task,
you finish it, and you come back to the application you came from.

That needs nothing built, because **`[Layout(…)]` is a per-page attribute**: the task's pages name the focused
layout, every other page names the app's own, and moving between them is an ordinary navigation.

```osy title="one app, two shells, one press apart" syntax
[Page("/")]            [Layout(AppLayout)]   component Home() { … }   // TabbedShell — the application
[Page("/claim")]       [Layout(ClaimLayout)] component Details() { … } // FocusedShell — the task
[Page("/claim/review")][Layout(ClaimLayout)] component Review() { … }
```

Set `chrome.Home` on the task's layout to the **application's** home, so the exit leaves the task and lands
somewhere real. A flow whose exit points at its own first step exits nothing.

⚑ **Some applications genuinely ARE one guided task** — a checkout, a tax filing, a permit application. That reading
is equally correct; there `chrome.Home` is the flow's own start, which is the honest answer when there is nowhere
else to go back to. `demo/shell-arrangements` ships both: the claim at `/t/claim/*` is the task-inside-an-app
reading, and the vehicle registration at `/f/*` is the standalone one.

### Where did the navigation go?   {#no-nav}

There is none, and that follows from the model rather than standing on its own: a route out of the task is an
invitation to abandon it. This is the one arrangement that puts **no** `nav` landmark on the page, which is worth
asserting in your own tests if you fork it — the way this shell decays is somebody "improving" it with a nav.

It still gets the two things a wandering shell cannot give a task:

- **Position in a sequence.** The app's own `Nav` is re-read as a linear flow (see below), so switching arrangement
  declares nothing new.
- **A way out**, always in the same place and always a real link to `chrome.Home`. A modal task with no visible exit
  is the most complained-about pattern in checkout design.

And **room**: the page is a centred column with a readable maximum rather than full-bleed. This is the one place in
the kit where centring is right, because there is exactly one thing to read.

⚠ **The exit does not yet protect unsaved work.** `chrome.Home` is a route, so leaving is a plain link and a
half-finished task is simply abandoned. Making it confirm needs something the contract cannot express today — a
shell cannot ask whether a named slot was filled, so a `slot exit` with a shell-drawn fallback is impossible, and an
`AppChrome.OnExit` would put a field on the shared record that three of four arrangements have no use for. Until
that call is made, a "Save draft" in `slot actions` is the pattern that works.

### How does the nav become steps?   {#steps}

`AppChrome.Steps(current)` numbers `Primary()` — the top level, with any `Section` replaced by its children — and
marks the entry holding the current route as active, everything before it as done. A top-level entry is a **step**;
its `Children` are that step's **parts**, nested under it while it is the active step, never as steps of their own.

⛔ **The stepper is a COLUMN beside the task, not a strip above it**, and that is a taxonomy decision rather than a
layout preference. Horizontal, it had the same silhouette as [TabbedShell](https://osysharp.com/reference/ui/shell-tabbed/)'s tab strip — a top bar, then a
full-width row of labelled items, then the page — and those two shells can sit **one press apart in the same
application**. Their models are opposites: tabs are parallel destinations you may choose in any order and which
persist for the life of the app; steps are sequential positions inside one task, which you cannot jump ahead in and
which vanish when the task ends. A row that looks like tabs invites the reader to click ahead to step four.

Down the side, it is unmistakably a sequence at a glance; it uses the room a bounded task column was otherwise
wasting; and it composes into the classic three-column checkout — **steps · task · summary**.

⚠ **Assert it geometrically or not at all.** `Assert.Visible("Receipts", within: "Progress")` is true of a step
wherever it sits, which is why a full suite stayed green while the silhouette was wrong. `Assert.Below` and
`Assert.LeftOf` are the only assertions that can see a shape.

⚠ **Scope to `"Current task"`, not to `"Page"`.** The stepper sits inside the page region, so `Page` means
*[steps | task]* and a geometric claim against it cannot tell the two columns apart.

⚠ **`Done` is order, not history.** The shell knows the flow's shape and never which steps the reader actually
completed — a step reads as done because it comes *before* the one holding the current route. An app that tracks
real completion should reorder or trim `Nav`; nothing in the shell can know it.

⚠ **A route the flow does not cover shows no position at all** — no counter, no stepper. That is right rather than a
gap: a confirmation page is genuinely outside the numbered part, and inventing a position for it would be a lie.

### What changes at each width?   {#bands}

| band | the bar | progress |
|---|---|---|
| compact `< 768` | mark · "Step 2 of 4" and the step's name · actions · identity · exit | a 4px rule |
| cozy `768+` | mark · actions · identity · exit | a **stepper column** left of the task: every step, ticked when done, with the active step's parts nested under it |
| wide `1100+` | the same | the same, and the `aside` joins as a third column — steps · task · summary |

⚑ **Mobile loses the stepper and nothing else.** Four labelled steps do not fit down the side of a 390px screen —
there is no side — so the phone gets the count and the name of the step it is on, which is the part that answers
"where am I". Every step is still counted, the exit is still there, and the summary still arrives under the page.

⛔ **Every piece of chrome must earn its place against "the task matters more than the application".** Three were
cut on this argument:

| cut | why |
|---|---|
| the brand **tagline** (`ShellBrand(tagline: false)`) | a workspace subtitle is a fact about the *application*; the stepper already names the task |
| the **progress rule** at pointer widths | a second answer to the question the stepper already answers richly. The phone keeps it, because there it is the only answer |
| the horizontal **stepper** | see above |

**Identity stays, and that is a decision rather than an oversight.** It is the only route to sign out and to the
appearance control, and a shell whose whole design is to trap the reader in a task is exactly the one where being
unable to reach your own account is worst. It is mark-only, so it costs 28px. The `search` slot is still *placed* —
most task apps will not fill it and an unfilled slot draws nothing, but a reader (a long document, a policy you must
accept) genuinely wants search within the task, and silently discarding a slot an app filled would lose content.

### Where the summary goes   {#aside}

`aside` matters more here than in any other arrangement: a checkout's order total or a wizard's context is the
canonical case, and it is why the page and its aside sit side by side from 1100px up. Below that it stacks under
the page rather than disappearing, because a phone that silently loses a panel has lost content, not layout.

## Examples       {#examples}

```osy title="asserting the omission, which is the feature" syntax
// Every other arrangement puts a `nav` landmark named "Main navigation" on the page. This one must not.
Assert.Hidden("Main navigation");

// The active step's PARTS are on screen; the parts of a step you are not on are not.
Ui.Visit("/f/receipts");
Assert.Visible("Upload", within: "Receipts parts");

// The page is a bounded column rather than full-bleed — invisible to any text assertion.
Assert.Narrower("Page", "Page header");
```

## Notes          {#notes}

⚠ **The stepper is a `region` named "Progress", not a second `nav`.** These are not places to go; they are where you
are in one task, and calling them navigation would put a navigation landmark in a shell that deliberately has none.

## See also       {#see-also}

- [App shells](https://osysharp.com/reference/ui/shell/) — the shared `AppChrome` contract every arrangement reads
- [TabbedShell](https://osysharp.com/reference/ui/shell-tabbed/) — top tabs, and a bottom bar on a phone
- [RailShell](https://osysharp.com/reference/ui/shell-rail/) — a permanent icon rail, when the canvas is the product
- [routes and pages](https://osysharp.com/reference/ui/routing/) — `[Page]`, `[Layout]`, `Outlet` and retention


---

<!-- https://osysharp.com/reference/ui/function-value/ -->

# Func<T, R>

> A parameter or class field typed `Func<T, R>` takes a lambda and can be invoked for a result, so reusable code can be told HOW to get a value rather than being handed one. It is an expression plus the values it captured, not a compiled closure — which is what keeps it checkable and what makes a typo a compile error.

<!-- id: ui-function-value · area: ui · stability: preview · html: https://osysharp.com/reference/ui/function-value/ -->

## Summary        {#summary}
A component that works over data it doesn't know the shape of needs to be told **how to get** a value, not just
which value:

```osy title="a component told how to read its label" test app=ui-function-value
[Composable] component Labelled(string name, Func<string, string> pick) {
  render { Text(pick(name)); }
}

[Page("/")] [AllowAnonymous]
component Home() {
  render {
    Stack(p: 4) {
      Labelled(name: "ada",   pick: x => x + " (picked)");
      Labelled(name: "grace", pick: x => "<" + x + ">");
    }
  }
}
```

`pick` is a **function value**. It is passed as a lambda and invoked with `pick(name)` wherever a value is wanted.

## Signature      {#signature}
```osy syntax
Func<T, R>          // a parameter that takes a lambda of one argument returning R
Func<T1, T2, R>     // …of two

pick(row)           // invoke it for its value
```

Contrast `Action` / `()`-delegate parameters, which are **callbacks**: they run and produce nothing. A `Func<>`
produces a value and can stand anywhere a value can.

## Description    {#description}

### It is an expression, not a closure   {#expression}
A function value is its **parameter names, its body, and the values it captured**. It is evaluated by binding the
parameters and evaluating the body — the same thing a query lambda (`Where(r => r.Total > 0)`) has always been.

### It captures the surrounding scope, **by value**   {#capture}
The body may read its parameters *and* whatever is in scope where you wrote it. Each outer name is read **once,
where the lambda literal appears** — not later, where it is invoked:

```osy title="an outer name is read where the lambda is WRITTEN, not where it runs" syntax
component Roster() {
  var admins = RoleGrant.Where(g => g.IsAdmin);       // the page's own query

  // `admins` is captured: read here, when this column list is built.
  var columns = [ new GridColumn<User> {
    Name = "Role",
    Value = u => admins.Any(g => g.User == u) ? "Admin" : "User"
  } ];
}
```

**Why by value, and not read later.** A function value travels: a column selector is handed to a grid and invoked
deep inside it, where the names your body reads do not exist at all — so a late read could only ever find nothing.
Reading at the literal is also the answer you want, because the expression that *built* the lambda re-evaluates when
its own inputs change. When `admins` reloads, the column list is rebuilt and a fresh selector replaces the old one.

The one thing to know: a captured value is a **snapshot**. If you mutate a captured list in place, a selector built
before the mutation keeps the value it was given.

### Everything about the call is checked   {#checking}
A slot's declared type is its contract, and all three ways of getting it wrong are compile errors:

| you wrote | you get |
|---|---|
| `pick: (a, b) => a.Title` for a one-argument slot | *takes `Func<Report, string>` — 1 parameter(s), but the lambda declares 2* |
| `pick: x => x.Amount` where the slot returns a string | *the lambda must produce string — it produces int* |
| `pick(a, b)` on a one-argument function | *this verb takes 1 argument(s) — got 2* |
| `var f = x => x;` — a lambda with nothing to bind `x` from | *a lambda needs a target type to bind its parameter from, and this position has none — the same rule as C#'s CS0815. Give it one: pass it to a parameter declared `Func<T, TResult>`, assign it to a member or variable declared with that type, or return it from a function whose return type is one.* |

So a renamed field breaks the build rather than quietly rendering a blank.

### Where it can be used   {#where}
A **function or method parameter**, a **component parameter**, and a **class field**. A lambda written at any of
them is bound from the declared type — the parameter says `Func<string, string>`, so `v` *is* a string and
`v.Length` type-checks:

```osy title="a lambda passed to a parameter" test app=ui-function-value-param
string Apply(Func<string, string> f) { return f("x"); }

class Tally {
  public int Hits = 0;
  public void Each(Action<int> a) { a(1); a(2); }   // an Action parameter takes a lambda too — run for its effect
}

[AllowAnonymous] int Count() {
  var suffix = "!";
  var shout = Apply(v => v + suffix);              // "x!" — the lambda may capture what is in scope
  var t = new Tally();
  t.Each(n => t.Hits = t.Hits + n);                // 3
  return t.Hits + shout.Length;
}
```

The class-field form is what lets a descriptor object carry its own selector, so a caller can describe a set of
columns, filters or sort keys as data:

```osy title="a descriptor object carrying its own selector" test app=ui-function-value-field
class Column {
  public Func<string, string> Value;
  public string Label;
}

[Composable] component Grid(string[] rows, Column[] columns) {
  render {
    Stack(gap: 2) {
      Row(gap: 3) { foreach (var h in columns) { Text(h.Label); } }
      foreach (var r in rows) {
        Row(gap: 3) { foreach (var c in columns) { Text(c.Value(r)); } }
      }
    }
  }
}

[Page("/")] [AllowAnonymous]
component Home() {
  string[] names = ["ada", "grace"];
  render {
    Grid(rows: names, columns: [
      new Column { Value = r => "<" + r + ">", Label = "Wrapped" },
      new Column { Value = r => r + "!",       Label = "Banged" }
    ]);
  }
}
```

The descriptor class can be **generic**, so one shape serves every row type instead of being copied per entity — see
[Generic classes](https://osysharp.com/reference/class/generics/):

```osy title="one generic descriptor instead of a copy per row type" syntax
class Column<T> { public string Label; public Func<T, string> Value; }
new Column<Report> { Label = "Title", Value = r => r.Title }
```

### It is not only a UI thing   {#outside-ui}
Every example above is a component, but a function value is an ordinary value in an ordinary function too: a class
field holds one, and invoking it produces a result wherever a value is wanted.

```osy title="held in a field, invoked in a plain function" test app=ui-function-value-server
class Report { public string Title; public Report(string title) { Title = title; } }
class Column<T> { public string Label; public Func<T, string> Value; }

string TitleOf(string title) {
  var col = new Column<Report> { Label = "Title", Value = r => r.Title };
  return col.Value(new Report(title));
}
```

The one limit: a function value **cannot be held across an `await` that suspends**. What would have to travel is a
body expression plus a captured environment, which is not a storable value — so invoke it before the await and hold
its result instead.

## Examples       {#examples}

```osy title="the same component, told two different things" test app=ui-function-value-two
[Composable] component Show(int n, Func<int, string> fmt) {
  render { Text(fmt(n)); }
}

[Page("/")] [AllowAnonymous]
component Home() {
  render {
    Stack(p: 4) {
      Show(n: 42, fmt: v => "n = " + v);
      Show(n: 42, fmt: v => "[" + v + "]");
    }
  }
}
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — parameters, state and the render block
- [Slot (child content)](https://osysharp.com/reference/ui/slots/) — the other way to let a caller decide content: a template rather than a value
- [Cell template (your own content in a control's cell)](https://osysharp.com/reference/ui/cell-template/) — per-item templates, which a function value complements rather than replaces


---

<!-- https://osysharp.com/reference/ui/scroll-extent/ -->

# Layout.ScrollHeight and Layout.ScrollWidth

> `Layout.ScrollHeight` is how tall a container's CONTENT is; `Layout.Height` is how tall the container is. The difference is what "is there more here than fits?" means, so `Layout.ScrollHeight > Layout.Height` is a scrollbar-present test, a "more below" hint, or a shadow that appears only when a list overflows. Both are `int?` and both are null on the server, which has no layout — write your own fallback with `?? n`.

<!-- id: ui-scroll-extent · area: ui · stability: stable · html: https://osysharp.com/reference/ui/scroll-extent/ -->

## Summary        {#summary}

`Layout.Height` answers *how big is this box*. `Layout.ScrollHeight` answers *how big is what is inside it*. When the
content fits, they are equal; when it does not, the extent is larger — and that difference is the only way to ask
whether a container is actually scrollable.

Both are **render-only** reads of the container the component is placed into, and both are **`int?`**: a server has no
layout, so SSR answers `null` rather than inventing a number.

## Signature      {#signature}

```osy syntax
int? h  = Layout.ScrollHeight;   // how tall the content is
int? w  = Layout.ScrollWidth;    // how wide the content is
```

Written **without parentheses** — they are measured values, not operations, the same reading that makes `Math.PI`
parenless.

## Description    {#description}

### Which box does it measure? — the container, not the root   {#what}

The same element `Layout.Width` / `Layout.Height` measure: **the box this component was placed into**, not the
component's own root. So a component that reads the extent reports on its *container*, which is what makes a reusable
"scroll hint" component possible — drop it inside any pane and it describes that pane.

That has one practical consequence worth knowing before you write it: reading the extent on a **page** answers a true
but useless number, because a page root grows to fit its content and the two readings are equal for ever. The extent
is interesting exactly where the box is **constrained**.

### When does it update? — on CONTENT, not on size   {#tracking}

Like every `Layout.*` read, this one subscribes the slot that read it — you write no wiring and the value updates.

⚑ **What it updates ON is not what the size reads update on**, and it is the reason this exists as its own primitive
rather than as an option on the others. The visible box changes when the element is **resized**. The extent changes
when the **content** changes — a row appended to a list inside a fixed-height pane grows the extent while the pane's
own box never moves at all. Both are handled; you do not have to know which one fired.

### Why is it null? — nothing has been measured yet   {#nullable}

`null` means *not measured* — on the server, or before the container exists. It is not a fallback the platform chose:
zero would divide, and any invented number would lay out at the wrong scale. Write the fallback yourself, where your
app can see it:

```osy syntax
// in a render block:
if ((Layout.ScrollHeight ?? 0) > (Layout.Height ?? 0)) { Text("more below"); }
```

## Examples       {#examples}

A "more below" hint that appears only when the list actually overflows — the canonical use, and one that cannot be
written from the box alone:

```osy test app=ui-scroll-extent
[AllowAnonymous]
component ScrollHint() {
  render {
    // Read IN the render block — `Layout.*` measures a rendered container, so a member initializer has nothing to
    // measure and the compiler refuses it there.
    Stack {
      if ((Layout.ScrollHeight ?? 0) > (Layout.Height ?? 0)) {
        Text("more below ↓", fontSize: 12);
      }
    }
  }
}

[Page("/inbox")]
[AllowAnonymous]
component Inbox() {
  int rows = 20;

  render {
    Stack(h: 200, overflowY: Overflow.Auto) {
      ScrollHint();
      foreach (var i in Enumerable.Range(0, rows)) {
        Text($"message {i}");
      }
    }
  }
}
```

## Limits — a `render` position only   {#notes}
- **Render position only.** The answer comes from measuring a rendered container, so it belongs in a `render` block —
  an action or a function body has nothing on screen to measure, and the compiler says so rather than letting it fail
  at run time.
- Reading an extent makes the platform measure the container, which forces layout. That cost is paid only by a
  component that asks for it — a page that never mentions `Layout.Scroll*` observes nothing.
- The extent includes content clipped by `overflow`, which is the whole point; it does **not** include margins outside
  the padding box.

## See also   {#see-also}
- [Layout.TextWidth](https://osysharp.com/reference/ui/text-measurement/) — `Layout.TextWidth`, for how wide a string will paint
- [layout primitives](https://osysharp.com/reference/ui/layout/) — `gap`, `align` and `justify`
- [style props](https://osysharp.com/reference/ui/styling/) — `overflowY`, `h`, and why a definite size on a flex child holds


---

<!-- https://osysharp.com/reference/ui/text-measurement/ -->

# Layout.TextWidth

> Answers how wide a string will actually paint, measured against the font the surrounding container paints with. Null wherever there is no font to measure against — the server, and any environment without text metrics — so the app supplies its own estimate with ?? and owns the fallback.

<!-- id: ui-text-measurement · area: ui · stability: stable · html: https://osysharp.com/reference/ui/text-measurement/ -->

## Summary        {#summary}
`Layout.TextWidth` tells you **how wide a string will paint**, before it is painted:

```osy syntax
render {
  // How much room does the longest label need? Reserve exactly that, and no more.
  Axis(gutter: Layout.TextWidth(longestLabel) ?? 40.0);
}
```

Almost every sizing question a UI asks has a layout answer — a flex child fills its share, an SVG viewBox maps a
nominal drawing onto whatever box it lands in, and neither needs a number in advance. Text is the exception. Whether
two axis labels **collide**, whether a centred readout will be **clipped**, and how wide a **gutter** a value label
needs are all decided before the box those things live in exists, and flexbox has no opinion to consult.

## Signature      {#signature}

| Form | Measures at |
|---|---|
| `Layout.TextWidth(text)` | the size the container **already paints at** |
| `Layout.TextWidth(text, fontSize)` | `fontSize` pixels, in the container's family and weight |

Returns a **nullable `double`**. `text` must be a `string`.

## Description    {#description}

### The size argument is optional, and omitting it is the safe form   {#font}

The number is only as good as the font it was measured with, and a measurement taken against the *wrong* font is not
an error — it is a plausible number that lays out slightly wrong. So the platform does not take the font from you.
It reads the **computed style of the container your component renders into**: family, weight, style, and — unless you
say otherwise — size.

Pass a size only when the text will genuinely be drawn at a size the container is not using (a heading you are about
to render, a canvas label). Family and weight still come from the container even then, because those are the parts an
author could not reliably supply anyway.

### Why is the measurement null? — no font on the server   {#nullable}

There is no font on a server, so a server-rendered page measures nothing. Rather than invent a number — which would
paint a layout built on a font nobody is using — the read answers **null**, and your `??` decides:

```osy syntax
double gutter = Layout.TextWidth(longest) ?? 40.0;
```

This is the same shape [layout primitives](https://osysharp.com/reference/ui/layout/)'s container reads use, and for the same reason: a *size* has no honest reading of
"not measured", so zero would be a lie that lays out. The estimate lives in your app, where you can see it.

The first client render corrects it, and so does the moment a **web font finishes loading** — until then the browser
can only measure the fallback face, so the answer is re-taken and anything that read it re-renders. Nothing is wired
by the app.

### Can I measure text in an action? — `render` only   {#render-only}

In a `render` block only. An `action` or a function body may run on the server and runs independently of any paint,
so it has no container and no font to measure against — that is a compile error, not a runtime surprise. Read it in
`render` and pass the number in.

### What it costs   {#cost}

Nothing on a page that never calls it, and no layout pass on a page that does: the measurement reads font metrics
directly rather than laying out a hidden element, and repeated measurements of the same string in the same font are
served from a cache. An axis measuring a dozen labels on every resize is a normal thing to write.

## Examples       {#examples}

Reserve exactly the gutter the longest tick label needs — and thin the labels out when they would collide:

```osy test app=ui-text-measurement
string Longest(string[] labels) {
  var best = "";
  foreach (var l in labels) { if (l.Length > best.Length) { best = l; } }
  return best;
}

// How many categories to skip so the widest label fits its slot. At least 1 — a step of 0 would show nothing.
int Step(double widest, double slot) {
  var needed = (int)(widest / slot) + 1;
  return needed < 1 ? 1 : needed;
}

[Page("/chart")]
[Render(CSR)]
[AllowAnonymous]
component MiniAxis() {
  string[] labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];

  render {
    // The gutter is exactly what the longest label needs — measured, not guessed at 40.
    var gutter = Layout.TextWidth(Longest(labels)) ?? 40.0;
    // Each category gets an equal slice of the plot. If the widest label does not fit its slice, show every Nth.
    var slot = ((Layout.Width ?? 600) - gutter) / labels.Length;
    var step = Step(Layout.TextWidth(Longest(labels)) ?? 40.0, slot);

    Row(gap: 1) {
      Box(w: gutter + "px") { Text("value"); }
      Stack {
        foreach (var i in Enumerable.Range(0, labels.Length)) {
          if (i % step == 0) { Text(labels[i]); }
        }
      }
    }
  }
}
```

The `?? 40.0` is not defensive clutter — it is what paints on the server and in the instant before the browser has
measured, so choose a number that looks right rather than a zero.

## See also   {#see-also}
- [layout primitives](https://osysharp.com/reference/ui/layout/) — the layout primitives, and the container reads (`Layout.Width` / `Layout.Height`) this is shaped after
- [Canvas](https://osysharp.com/reference/ui/canvas/) — drawing text yourself, where you also choose the size it is drawn at
- [keys](https://osysharp.com/reference/ui/keys/) — the other primitive that answers a question about the live page


---

<!-- https://osysharp.com/reference/ui/markdown/ -->

# Markdown — rendering markdown text

> `Markdown(text)` renders a markdown string as formatted content — headings, lists, tables, code, links. It is a read-only renderer, not an editor: use it for a description field, a help panel, release notes, a chat message, or any stored text an author wrote in markdown. Markup inside the document is shown as text, never treated as markup, so a document is always content and never a way into the page.

<!-- id: ui-markdown · area: ui · stability: stable · html: https://osysharp.com/reference/ui/markdown/ -->

## Summary        {#summary}
Markdown is how people write text that has shape — a heading, a list, a link, a table. `Markdown(text)` takes such a
string and renders it:

```osy syntax
Markdown(article.Body)
```

It is a **renderer, not an editor**. It has no toolbar, no cursor and no storage: you give it a string and it draws
the document that string describes. That makes it the right thing for the many places that want formatted text and
nothing else — a product description, a help panel, a policy page, release notes, an agent's reply.

The rendered document inherits your app's [theme tokens](https://osysharp.com/reference/ui/theming/) tokens, so its headings use your heading face, its links
take your primary colour, and it follows your app through light and dark without being told to.

## Signature      {#signature}
```osy syntax
Markdown(<string expression>)                       // render a settled document

Markdown(text, streaming: <bool>)                   // the document is still arriving
Markdown(text, copyIcon: <icon>, copyTooltip: "…")  // give every code block a copy button
```

The first argument is an ordinary expression — a property, a member, a literal, the result of a call. There is
nothing to declare and nothing to register. The two options are described below and both default to off.

## Description    {#description}

### It renders whatever the string says   {#syntax}

Everything below is understood. Anything else is shown as the text the author typed.

| | |
|---|---|
| **Headings** | `# One` … `###### Six`, and the underlined form. A trailing `{#custom-id}` becomes the heading's id, so a `#link` can land on it |
| **Text** | `*emphasis*`, `**strong**`, `~~struck~~`, `` `code` ``, and a line ending in two spaces for a hard break |
| **Lists** | bullets and numbers, nested, tight or spaced; `- [ ]` / `- [x]` render a checkbox |
| **Blocks** | fenced code with its language, indented code, block quotes, horizontal rules |
| **Tables** | the pipe form, including per-column alignment |
| **Links** | `[text](url)`, bare `<https://…>`, `<name@example.com>`, and images `![alt](url)` |
| **Alerts** | `> [!NOTE]` and `TIP`, `IMPORTANT`, `WARNING`, `CAUTION` — the GitHub callout boxes |

#### Alerts — a callout box    {#alerts}

A block quote whose first line is a marker on its own becomes a coloured callout:

```md
> [!WARNING]
> This deletes the record and cannot be undone.
```

The marker is case-insensitive, and the title is supplied for you — the five kinds are the whole vocabulary, and
`NOTE`, `TIP`, `IMPORTANT`, `WARNING` and `CAUTION` are exactly the ones GitHub renders, so a document written for
one reads the same in the other.

**A marker that is not one of the five is not an alert.** `> [!WARNIGN]` stays an ordinary quote with its marker
visible, which is what every reader that has never heard of alerts already does. That is deliberate: a typo turning
into a confident box titled *"Warnign"* would look intentional, and a wrong-looking callout is worse than a plain
quote.

It renders as a box with its own role rather than a tinted quote, because a quote means *someone else said this* and
an alert means *the author is raising their voice* — and a screen reader announces the two differently. Each kind
takes its colour from one token (`Colors { Markdown { AlertWarning } }`), which sets the bar, the title and the
tint together.

⚠ The markdown **editor** does not yet style these — it round-trips them safely and shows the marker as text, so a
document is never damaged by being edited there; it just does not draw the box.

Three things are deliberately NOT rendered. **Raw HTML is shown as text** — see below, it is the security rule.
**Footnotes** (`[^1]`) are not yet formatted and appear as written; the markdown *editor* does handle them, so a
document may contain one. **Math** (`$x^2$`, `$$…$$`) is the same: the editor renders formulae, this renderer shows
their source. That difference is deliberate rather than pending — drawing maths needs a typesetting library, and this
renderer is part of the platform's client, which carries no third-party code at all. An editor is an app's own
component and can choose to ship one.

#### A rule under the title    {#heading-rule}

Long documents read better when the title area closes off. Set one token and the document's opening heading gets a
hairline underneath:

```osy title="one token puts a hairline under the document's title" syntax
theme Default {
  Colors { Markdown { HeadingRule = "#E5E3DC"; } }
  Space  { Markdown { HeadingRuleGap = "0.3em"; } }   // optional breathing room; omit for a flush line
}
```

**The LEADING `h1` only** — the document's title. A second `h1` further down takes no line, and neither does an
`h2`: this closes the title area, it is not heading decoration. A document that does not open with an `h1` gets no
rule at all, which is correct — it has no title area. Use `HeadingRuleGap` to make the line *close* the title area
rather than underline the words.

**It is off unless you ask for it, and off costs nothing.** With no `HeadingRule` declared the headings are exactly
what they were: no line, and — the part that matters — *no space reserved for one*. A document you are already
rendering does not move by a pixel because this feature exists.

⚠ Do not confuse this with a `---` in the text. That is a **thematic break**, part of the document, and it renders
as a full-width rule wherever the author put one (`Colors { Markdown { Rule } }`). The heading rule is your theme's
opinion about headings; the thematic break is the author's content. A tool like Craft draws both, which is why they
can look like the same feature.

### A document is content, never markup   {#no-html}

This is the rule worth knowing, because it is what makes the atom safe to point at data:

> Anything in the document that looks like HTML reaches the page as **text**. `<b>` renders as the four characters
> `<b>`, not as bold. A `<script>` is four-and-a-bit characters of visible text, not a script.

Text in a database is written by people, and on this platform sometimes by agents. If a stored description could
smuggle a `<script>` into the page that shows it, then every page that shows a description would be a hazard. So it
cannot: there is no setting that turns raw HTML on.

It also happens to be what authors mean. A sentence containing `List<string>` renders as a sentence containing
`List<string>`, rather than losing the word to a tag nobody wrote.

Links are held to the same rule: a URL that is not an ordinary web link is rendered inert rather than followed, so
`[click](javascript:…)` shows the word "click" and goes nowhere.

### Styling it   {#styling}

**Placing it.** The atom takes the same [style props](https://osysharp.com/reference/ui/styling/) props as anything else — most often a reading measure:

```osy title="placing the atom — usually just a reading measure" syntax
Markdown(article.Body, maxW: 672, mx: "auto")
```

**Its look, in three tiers.** You will usually only need the first.

1. **Do nothing.** The document reads in your app's voice already: it takes `Fonts { Heading }` for its headings,
   `Colors { Primary }` for its links, `Colors { Muted }` behind code, `Colors { Border }` for rules and table
   borders, and your radii.
2. **Move a semantic token** and the document moves with everything else — change `Colors { Primary }` and its links
   follow, because that is where they came from.
3. **Give the document its own value** by nesting a `Markdown` group inside the token's category. A nested group
   namespaces *within* that category, so it never disturbs the token it overrides for everyone else:

```osy title="giving a DOCUMENT its own tokens without disturbing the app's" syntax
theme Docs {
  Colors {
    Primary = "#0F766E";                  // the app's links, buttons, focus rings
    Markdown { Link = "#B45309"; }        // …but a DOCUMENT's links, only
  }
  FontSize   { Markdown { H1 = "2.4rem"; H2 = "1.7rem"; H3 = "1.3rem"; } }
  FontWeight { Markdown { HeadingWeight = 700; } }
  Space      { Markdown { Block = "1.25em"; HeadingTop = "2em"; ListIndent = "1.8em"; } }
  Length     { Markdown { LineHeight = "1.75"; } }
}
```

The full set, each falling back to the app token in brackets and then to a built-in default:

| Category | Keys |
|---|---|
| `Colors { Markdown { … } }` | `Link` *(Primary)* · `CodeBg` *(Muted)* · `TableHeadBg` *(Muted)* · `TableBorder` *(Border)* · `Rule` *(Border)* · `QuoteBar` *(Border)* · `QuoteText` *(TextMuted)* · `AlertNote` · `AlertTip` · `AlertImportant` · `AlertWarning` · `AlertCaution` · `HeadingRule` |
| `FontSize { Markdown { … } }` | `H1` · `H2` · `H3` · `H4` · `CodeSize` |
| `FontWeight { Markdown { … } }` | `HeadingWeight` · `TableHeadWeight` · `AlertTitle` |
| `Fonts { Markdown { … } }` | `HeadingFace` *(Fonts.Heading)* · `CodeFace` *(Fonts.Mono)* |
| `Space { Markdown { … } }` | `Block` · `HeadingTop` · `HeadingBottom` · `ListIndent` · `ItemGap` · `CodePad` · `CellPad` · `QuotePad` · `RuleGap` · `AlertPad` · `AlertTitleGap` · `HeadingRuleGap` |
| `Length { Markdown { … } }` | `LineHeight` · `HeadingLineHeight` · `QuoteBarW` · `AlertBarW` · `HeadingRuleW` |
| `Radius { Markdown { … } }` | `CodeRadius` *(Radius.Sm)* · `BlockRadius` *(Radius.Md)* |

> **Token names are global.** A theme's leaf names must each denote one value — a bare `Heading` in a variant has to
> mean something definite — so a nested group namespaces the *variable*, not the *name*. That is why these read
> `HeadingFace` and `HeadingWeight` rather than `Heading` twice: an app almost certainly already has
> `Fonts { Heading }`, and a collision is a compile error naming both groups.

### Rendering a reply while it is still streaming in   {#streaming}
An agent's reply arrives a few characters at a time, and a document being typed is briefly not valid markdown: the
`**` of a bold run has no closing pair yet, a `|` is not yet a table. Rendering that literally shows the reader the
raw syntax for a frame or two, which looks like a glitch.

Say the text is still arriving and the renderer holds the unfinished tail — an in-progress construct is drawn as
though it were already closed, and a caret marks the end:

```osy title="a reply as it arrives" test app=ui-markdown-streaming
component Reply(string Body, bool Done) {
  render {
    Markdown(Body, streaming: !Done);
  }
}
```

Two things are worth knowing:

- **`streaming: false` is the half that matters.** The renderer can tell that text was appended, so it can stabilise
  a half-typed construct by itself. What it cannot tell is that the stream has *ended* — so turning the flag off is
  what settles the document, re-rendering the tail as ordinary markdown. A document left permanently `streaming: true`
  keeps its caret forever.
- It does not make rendering faster. Rendering is already incremental: editing one paragraph of a long document
  repaints that paragraph, whether or not anything is streaming.

### A copy button on every code block   {#code-actions}
A document full of commands or snippets wants a copy button on each one. Your app cannot reach inside the rendered
document to add one, so ask for it here — and supply the icon and the words yourself, because they belong to your
design and your language:

```osy title="copyable code blocks" test app=markdown-demo
component Guide(string Body) {
  render {
    Markdown(Body, copyIcon: copy, copyTooltip: "Copy code");
  }
}
```

`copyIcon` is one of your app's own [icons](https://osysharp.com/reference/ui/icons/) — a bare name, checked at compile time like any other icon, so a glyph you have not declared is a compile error rather than an empty square on the page. The
tooltip is also the button's accessible name, which is why the two are required **together**: an icon with no
tooltip is a button nobody using a screen reader can identify, and a tooltip with no icon describes a control that
never appears.

It is **opt-in on purpose**. A markdown atom is otherwise inert output — no buttons, no state — and that is a large
part of why it is safe to point at text an agent or a customer wrote. A help panel should not sprout controls
because it happened to contain a fenced block.

The button appears on hover, and on keyboard focus so it is reachable without a mouse. On touch, where there is no
hover, it is always visible. To copy text that is **not** inside a rendered document, use [Clipboard](https://osysharp.com/reference/ui/clipboard/).

### Where the text comes from   {#source}

Anywhere. A `Markdown` property on an entity is the common case — a document-backed member whose text is stored as
sections — but a plain `string` works exactly as well, and so does a value your code just computed.

## Examples       {#examples}

A product description rendered under its name:

```osy title="a description field" test app=ui-markdown
entity Article {
  [Required] [MaxLength(200)] string Title;
  string Body;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

[Page("/article/{id}")]
[Render(SSR)]
component ArticlePage(Guid id) {
  var article = Article.Where(a => a.Id == id).FirstOrDefault();

  render {
    Stack(gap: 3, maxW: 720, mx: "auto") {
      Text(article.Title, fontSize: 32);
      Markdown(article.Body);
    }
  }
}
```

Text the page itself holds — a help panel that opens and closes, with no storage behind it at all:

```osy title="a help panel" test app=ui-markdown-help
[Page("/help")]
[Render(CSR)]
component HelpPage() {
  bool open = false;
  string help = "## Getting started\n\n1. Create a project\n2. Add a page\n3. Compile\n\nSee the **guide** for more.";

  action Toggle() { open = !open; }

  render {
    Stack(gap: 2) {
      Button(open ? "Hide help" : "Show help", onPress: Toggle);
      if (open) { Markdown(help, maxW: 640); }
    }
  }
}
```

## See also       {#see-also}
- [Clipboard](https://osysharp.com/reference/ui/clipboard/) — `Clipboard.Copy(text)`, for copying anything outside a rendered document
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the tokens a rendered document reads for its typefaces and colours.
- [style props](https://osysharp.com/reference/ui/styling/) — the style props the atom takes, like any other element.
- [entity members](https://osysharp.com/reference/entity/properties/) — declaring the property the text lives in.
- [component](https://osysharp.com/reference/ui/component/) — the component the atom is written inside.


---

<!-- https://osysharp.com/reference/ui/render-binding/ -->

# Naming a value in render

> A `render` block can name a value the way any C# block does — `var lapsed = …;` to infer the type, or `bool lapsed = …;` to declare it. The name is in scope for the rest of the block, so a value read twice is written once. A declared type pins the binding and is checked exactly as it is in a method body.

<!-- id: ui-render-binding · area: ui · stability: stable · html: https://osysharp.com/reference/ui/render-binding/ -->

## Summary        {#summary}

Inside `render { … }` a value can be given a name:

```osy syntax
var lapsed = DateTime.UtcNow > deadline;   // inferred from the initializer
bool lapsed = DateTime.UtcNow > deadline;  // declared, and checked
```

Both are the same binding. The name is visible to the **following siblings** in the block — not to anything before
it, and not outside it — which is the scope a C# local has.

## Signature      {#signature}

```osy syntax
var  <name> = <expression>;    // the type is inferred from the initializer
<Type> <name> = <expression>;  // the type is declared, and the initializer is checked against it
```

## Description    {#description}

### Why name a value at all   {#purpose}

A render expression is read where it is written, so a value used twice is otherwise written twice. Naming it once
is shorter to read and impossible to get subtly different in the second copy.

### Inferred or declared   {#typing}

`var` takes the initializer's type. A declared type **pins** the binding, which is not always the same thing:

```osy syntax
var half = 1;        // int    — `half / 2` is 0
decimal half = 1;    // decimal — `half / 2` is 0.5
```

The declared type is checked with the same rule a method body uses: the initializer must be assignable to it. A
derived value goes into a base-typed binding, a literal takes the C# constant conversion, and there is no implicit
conversion between `decimal` and `double` in either direction. An initializer that does not fit is a compile error
naming both types.

A binding must be initialized where it is declared — there is no definite-assignment analysis, so `bool flag;`
on its own is refused.

### What a binding cannot do   {#limits}

A binding names a value; it does not introduce a data read. Its initializer is held to the same client-runnable
rule as every other render expression, so it cannot reach the server, write state, or run an effect. A value that
needs a query belongs in a `live var` component field.

## Examples       {#examples}

A status label and a flag, each computed once and read several times:

```osy test app=ui-render-binding
[Page("/requests")]
[Render(CSR)]
[AllowAnonymous]
component Requests() {
  int[] ages = [1, 2, 5];

  render {
    Stack {
      // Declared, because the type is the point of the value.
      string heading = "Requests";
      Text(heading);

      foreach (var age in ages) {
        // Read twice below — written once here.
        bool lapsed = age > 3;
        Text($"{age}d: {(lapsed ? "lapsed" : "open")}{(lapsed ? " (closed)" : "")}");
      }
    }
  }
}
```

## See also       {#see-also}

- [component](https://osysharp.com/reference/ui/component/) — component fields, including `live var` for values that read data
- [Calling helpers from render](https://osysharp.com/reference/ui/render-calls/) — calling a pure helper from a render expression


---

<!-- https://osysharp.com/reference/ui/navigation/ -->

# Navigation

> The routes the user currently has open, and the verbs that move between them. Read `Navigation.Routes` in a layout to build a tab bar, a breadcrumb, or a back-stack; call `Navigation.Go` and `Navigation.Close` to act on them, or `Navigation.Open` to leave for an external address. The platform supplies the facts and the verbs, and renders none of the chrome.

<!-- id: ui-navigation · area: ui · stability: stable · html: https://osysharp.com/reference/ui/navigation/ -->

## Summary        {#summary}
`Navigation` is available in every component. It tells you which routes the user has **open**, which one they are
**looking at**, and which ones hold **unsaved edits** — and it lets you open and close them.

It exists so you can build the chrome that names and switches between pages: a tab bar, a breadcrumb, a mobile
back-stack, a page header. The platform ships **none of that chrome**. It ships the facts.

## Signature      {#signature}
```osy syntax
Navigation.Routes         // the open routes, in the order they were opened
Navigation.CurrentPath    // the address of the active route, or null before the first page mounts
Navigation.Hash           // the URL fragment, decoded and without the leading `#` (empty when there is none)
Navigation.IsDirty        // does the CURRENT page hold uncommitted edits? (a bool)

Navigation.Go(path)               // navigate to a route OF THIS APP — checked against the pages it declares
Navigation.Open(url)              // open an EXTERNAL address in a new tab
Navigation.Open(url, sameTab: true)   // …or leave the app in THIS tab, replacing the page
Navigation.Close(path)            // close an open route; false if it has unsaved edits
Navigation.Close(path, force)     // close it regardless
Navigation.Save(path)             // commit that route's own edits, then close it
Navigation.SetTitle(title)        // name THIS route — what its tab and the browser title show
```

Each entry in `Navigation.Routes` has four fields:

| Field | Type | Meaning |
|---|---|---|
| `Path` | `string` | The route's address (`/users`, `/apps/3f2a`) — what you pass back to `Go` and `Close`. |
| `Title` | `string` | The page's name: whatever it last passed to `Navigation.SetTitle`, else its static `[Title("…")]`, else null. |
| `IsActive` | `bool` | This is the route the user is looking at. Exactly one open route is active. |
| `IsDirty` | `bool` | The page has uncommitted edits — closing it would discard them. |

## Description    {#description}

### `Go` is internal, `Open` is external    {#internal-external}
The two navigation verbs split on one question: **is the address a page of this app?**

`Go` takes a route this app declares, so its literal argument is **checked at compile time** against the app's own
`[Page("…")]` declarations — a typo names the routes that do exist instead of navigating to nothing. A route built at
runtime (`"/org/" + slug`) is not checked, because there is nothing to check it against.

`Open` takes an address that is **not** a page of this app — an external site, a download link, a server endpoint —
and is never checked against the app's routes. By default it opens beside your work, in a new tab. Pass
`sameTab: true` to leave the app in the current tab instead:

```osy syntax
Navigation.Go("/settings");                                   // a page of this app
Navigation.Open("https://docs.example.com");                  // a new tab, beside the app
Navigation.Open("/api/oauth/authorize?provider=Google", sameTab: true);   // leave, and come back
```

The `sameTab` case is for a redirect you **return from**. An OAuth authorize endpoint sends the visitor to the
provider and brings them back to the app; a new tab cannot serve that, because the original tab sits there
unchanged — still logged out. A download or a reference page wants the default: it opens beside the work in
progress rather than replacing it.

Both forms of `Open` sanitize the address against the same scheme allow-list an `href` uses, so a `javascript:`
address is refused however it reached the call.


### What counts as "open"    {#open}
An outlet holds one page at a time unless you ask it to keep more (see [routes and pages](https://osysharp.com/reference/ui/routing/) `#retain`). Either way,
`Navigation.Routes` reports the routes that are **currently open**:

```osy syntax
Outlet;                  // one open route — the page being viewed
Outlet(retain: true);    // every visited route stays open
Outlet(retain: 8);       // …up to 8
```

So a plain shop or marketing layout — which never opts into retention — can still read the current page's title for
a header or a breadcrumb. It just always sees exactly one route.

### Does my chrome re-render when a route changes?   {#reactivity}
Reading `Navigation` in a component's `render` subscribes that component to it. When a route opens, closes, becomes
active, or its page becomes dirty, the component re-renders. An unsaved-work dot next to a tab stays truthful without
you polling anything.

A component that never mentions `Navigation` subscribes to nothing and costs nothing.

### Can I read `Navigation` inside an action?   {#in-actions}
An action reads `Navigation` the same way `render` does — where you are and whether the page is dirty are simply
available, so a guard can ask for itself instead of being told:

```osy title="a navigate-away guard" test app=ui-navigation
[Page("/guarded")] [Render(CSR)]
component Guarded() {
  string blockedPath = "";
  action Leave(string path) {
    if (path != Navigation.CurrentPath && Navigation.IsDirty) { blockedPath = path; }
    else { Navigation.Go(path); }
  }
  render {
    Pressable(onClick: () => Leave("/users")) { Text("Users"); }
    if (blockedPath != "") { Text("Unsaved changes"); }
  }
}
```

The action sees the router **as it is when the action runs** — not as it was when the screen was drawn. That is the
difference that matters: a value captured at render time and carried into a handler is a snapshot, and a stale one is
exactly what a navigate-away guard must not act on. Only the target has to travel.

### Is there anything to save? — `Navigation.IsDirty`   {#is-dirty}
`Navigation.IsDirty` is a `bool`: **does the current page hold uncommitted edits?** It is the active route's own
`IsDirty`, read directly — the common case where a page gates its own **Save** on whether there's anything to save.
Like the per-route field it reflects, it updates itself: bind a Save button's `disabled` to it and the button lights up
the moment the user changes something and dims again after a successful save.

```osy title="save gated on dirty" test app=ui-navigation
[Principal] entity User { string Email; }
entity Organization { [Required] string Name; string Slug; }

[Page("/settings/{slug}")] [Render(CSR)]
component Settings(string slug) {
  var org = Organization.Single(o => o.Slug == slug);
  action Save() { UnitOfWork.Commit(); }
  live var canSave = Navigation.IsDirty && Validation.Violations.Count == 0;   // something to save, and it's valid
  render {
    Pressable(onClick: Save, disabled: !canSave) { Text("Save"); }
    Input(value: org.Name);
  }
}
```

It is `false` on a freshly-served page (SSR) and until the first edit.

### Closing is where unsaved work is protected    {#closing}
Switching between open routes never discards anything: the page you leave stays mounted, with its state and its
half-typed form intact. **Closing** is the discard point, so `Close` is deliberately awkward about it:

```osy title="close, then ask" test app=ui-navigation
[Page("/tabs")] [Render(CSR)]
component TabCloser() {
  string discarding = "";
  action CloseTab(string path) {
    // The page has unsaved edits. Ask, in your own words, in your own dialog.
    if (!Navigation.Close(path)) { discarding = path; }
  }
  action ConfirmDiscard() { Navigation.Close(discarding, true); discarding = ""; }
  render {
    Pressable(onClick: () => CloseTab("/users")) { Text("Close"); }
    if (discarding != "") { Pressable(onClick: ConfirmDiscard) { Text("Discard"); } }
  }
}
```

`Navigation.Close(path)` returns **false** — and changes nothing — when the page has uncommitted edits. Passing
`force` closes it anyway. The platform never shows a confirmation dialog, because it has no business deciding what
your app's dialogs look like or what they say.

`Close` also returns false for a path that isn't open, and for **every** path when your outlet retains nothing: a
single open route has nothing to fall back to, so there is nothing a close could reveal.

**`Navigation.Save(path)`** is the other answer to the close prompt: it **commits that route's own unsaved edits**,
then closes it. A page's edits live in the page's own unit of work, which your chrome can't reach — but the router can,
so `Save` is how a "Save before closing?" prompt keeps the work instead of discarding it. So a close prompt has three
natural answers — **Save** (`Navigation.Save(path)`), **Discard** (`Navigation.Close(path, true)`), and **Keep editing**
(dismiss your prompt).

**A save can be refused**, and `Navigation.Save` tells you so: the values may break the entity's own rules, or the
server may say no. It **raises**, so catch it and say why — the route stays open with the edits intact, and the user can
fix them. A prompt that closed itself while the save failed would look exactly like a save that worked, which is the one
thing it must never do.

```osy title="the three answers to a close prompt" test app=ui-navigation
[Page("/save-prompt")] [Render(CSR)]
component SavePrompt() {
  string closing = "/users";
  string saveError = "";
  action SaveAndClose() {
    try { Navigation.Save(closing); closing = ""; }
    catch (ValidationException ex) { saveError = ex.Message; }  // your prompt says why; the route stays open
  }
  action Discard() { Navigation.Close(closing, true); }         // throw them away, then close
  action KeepEditing() { closing = ""; }                        // just dismiss the prompt
  render {
    Pressable(onClick: SaveAndClose) { Text("Save"); }
    Pressable(onClick: Discard) { Text("Discard"); }
    Pressable(onClick: KeepEditing) { Text("Keep editing"); }
    Text(saveError);
  }
}
```

See [Validation](https://osysharp.com/reference/ui/validation/) for what a refusal carries — `ex.Violations` names each field and the rule it broke, so a form can
put the message beside the control that produced it.

### Moving to another page — `Navigation.Go`   {#navigating}
`Navigation.Go(path)` moves to a route exactly as clicking an in-app link would: the address bar updates, Back and
Forward work, and no page reload happens. It returns immediately — the destination's data loads on its own — so an
action that navigates does not wait for the next page.

### Leaving the app — an external url    {#external}
`Navigation.Go` moves **within** your app. `Navigation.Open(url)` leaves it: it opens an external address in a **new
tab**, so the page the user is on — and anything they have typed into it — stays exactly where it was.

```osy title="opening an address outside your app" syntax
Navigation.Open("https://docs.example.com/getting-started");
```

Reach for it when the address **cannot be known while the page renders**, which is the case a plain link cannot
cover. The usual example is a download: a file's link is minted on demand, is signed, and expires shortly after, so
there is nothing to put in a link until the moment someone asks for it. Mint it in the action, then open it:

```osy title="mint the url, then open it" test app=ui-navigation
entity Document {
  [Required] string Name;
  security { allow create, read, update when IsAuthenticated; }
}

string GetDownloadUrl(Guid fileId) {                // signed and short-lived, so it cannot be a link
  return "https://files.example.com/" + fileId.ToString();
}

[Page("/files")] [Render(CSR)]
component Files() {
  var docs = Document.OrderBy(d => d.Name).ToList();
  action Download(Guid fileId) {
    string url = GetDownloadUrl(fileId);      // minted now, valid for a few minutes
    Navigation.Open(url);
  }
  render {
    foreach (var d in docs) {
      Pressable(onClick: () => Download(d.Id)) { Text(d.Name); }
    }
  }
}
```

When the address **is** known as the page renders, prefer an ordinary link — it is a real link, so the browser can
offer "open in new tab", copy it, and show where it goes:

```osy title="the address is known at render — use a link" syntax
Link(href: "https://example.com") { Text("Example"); }
```

Only ordinary web addresses open — `https:`, `http:`, `mailto:` and `tel:`. Anything else is refused rather than
opened, so a url that arrived from your data can never be turned into something executable. A refused url is
reported in the browser console, so a link that does not open tells you why instead of failing silently.

### What is open on a cold load, and after a refresh?   {#first-load}
When a browser first lands on one of your pages from the outside — a bookmark, a shared link, a brand-new tab —
exactly one route is open: the one being served. `Navigation.Routes` reflects that on the server-rendered first paint
too, so your chrome paints with the page instead of appearing a moment later.

A **refresh** of a running session is different. A retaining outlet (`Outlet(retain: …)`) remembers the tabs you had
open, so a reload re-opens all of them — the served route stays active and the rest come back alongside it, in the
same order — instead of collapsing to the single page the browser happened to reload. The tab **set** is what
survives; a tab's unsaved edits are not (a reload discards the in-memory overlay, and the browser warns you first).
A plain `Outlet;` keeps nothing, so it always lands on exactly the one served route.

### What is a tab called? — `Title` and `SetTitle`   {#titles}
`Title` comes from the page's `[Title("…")]`. A page that declares none reports null, and it is up to your chrome to
decide what to show — its path, a fallback label, or nothing.

```osy title="a static route name" test app=ui-navigation
[Page("/users")] [Title("Users")] [Layout(AppShell)] [Render(CSR)]
component UsersPage() {
  render { Text("Users"); }
}
```

When the name depends on the page's **data** — an editor titled by the record it's editing — call
`Navigation.SetTitle(title)` from an [on change](https://osysharp.com/reference/ui/on-change/) block. The reaction re-runs as the data changes, so the name follows it:

```osy title="a name that follows the data" test app=ui-navigation
[Page("/orgs/{slug}")] [Render(CSR)]
component OrgEdit(string slug) {
  var org = Organization.Single(o => o.Slug == slug);
  on change { Navigation.SetTitle(org.Name); }     // the tab + the browser title track the org's name
  render { Input(value: org.Name); }
}
```

`SetTitle` names the route the calling page is mounted in. It feeds `Navigation.Routes[].Title` — so whatever chrome
you render from that (a tab strip, a breadcrumb) follows along — and it drives the **browser** title of the active
route. `[Title("…")]` remains the fallback: what's shown server-rendered, and before the reaction first runs. Setting
the same title twice does nothing, so re-running the `on change` block costs nothing.

### Reading what came after the `#` — `Navigation.Hash`   {#hash}
`Navigation.Hash` is everything after the `#` in the current address, URL-decoded and without the leading `#` — so at
`/oauth/complete#pending_oauth=abc` it reads `pending_oauth=abc`. It is empty when there is no fragment, and empty
server-side, where there is no location at all.

It exists for the one case a query string cannot cover: a **fragment is never sent to the server**, so it is the right
carrier for a one-time bearer value handed back to a page by an external redirect. Pull a value out of it with
`Text.Split`:

```osy syntax
string token = Text.Split(Navigation.Hash, "pending_oauth=")[1];
```

### Can I name something `Navigation`? — shadowing   {#shadowing}
`Navigation` is an ambient name, not a keyword. A parameter, state member, or query named `Navigation` shadows it,
exactly as a local variable shadows any other ambient. Nothing is reserved.

## Examples       {#examples}

### Building a tab bar — there is no `Tab` component    {#example-tabs}
Everything below is ordinary Osy#. There is no `Tab` component in the platform, and this is the whole reason:
you write the tab bar you actually want.

```osy title="a tab bar" test app=ui-navigation
[Layout]
component AppShell() {
  string discarding = "";

  action Close(string path) {
    if (!Navigation.Close(path)) { discarding = path; }   // your own confirm, your own state
  }
  action ConfirmDiscard() { Navigation.Close(discarding, true); discarding = ""; }

  render {
    Row(gap: 1) {
      foreach (var r in Navigation.Routes) {
        Row(gap: 1) {
          Link(href: r.Path) { Text(r.Title); }            // navigating is what an anchor is for
          if (r.IsDirty) { Text("•"); }
          Pressable(onClick: () => Close(r.Path)) { Text("×"); }
        }
      }
    }
    if (discarding != "") { Pressable(onClick: ConfirmDiscard) { Text("Discard"); } }
    Outlet(retain: 8);
  }
}
```

Each × closes **its own** tab: `() => Close(r.Path)` binds the argument in the loop, so every row's handler carries
that row's path. See [component](https://osysharp.com/reference/ui/component/) for the handler form.

### A breadcrumb, with no retention at all    {#example-breadcrumb}
```osy title="a breadcrumb" test app=ui-navigation
[Layout]
component ShopShell() {
  render {
    Row(gap: 1) {
      Text("Shop");
      foreach (var r in Navigation.Routes) { Text(r.Title); }   // exactly one: the page being viewed
    }
    Outlet;
  }
}
```

## See also {#see-also}
- [routes and pages](https://osysharp.com/reference/ui/routing/) — route templates, in-app navigation, and `Outlet(retain: …)`.
- [layout primitives](https://osysharp.com/reference/ui/layout/) — declaring the chrome a page renders inside.
- [component](https://osysharp.com/reference/ui/component/) — components, `render` blocks, and actions.


---

<!-- https://osysharp.com/reference/ui/kit/ -->

# Osysharp.Ui (the UI kit)

> The bundled UI kit — ready-made styled controls like `Button`, the shared design-system vocabularies (`Tone`, `Size`) and a starter theme — is in scope for every app with nothing to write: no `using`, no `use`. You reference a kit control just like your own components, and you can fork any control by declaring one with the same name.

<!-- id: ui-kit · area: ui · stability: stable · html: https://osysharp.com/reference/ui/kit/ -->

## Summary        {#summary}
The **UI kit** — a bundled set of ready-made, styled controls and the shared vocabularies they use — is **in scope
for every app**. There is nothing to write: no `using`, no `use`. Drop a kit control straight into a `render`
block:

```osy title="a kit control, dropped straight in" test app=ui-kit
[Page("/")]
[AllowAnonymous]
[Render(CSR)]
component Home() {
  action Save() { }
  render {
    Stack(align: Align.Center) {
      Button("Save", onPress: Save);
    }
  }
}
```

`Button` is a kit control — it isn't declared in your app, and it needs no import. (`align: Align.Center` is built
in too, and always was. See [layout primitives](https://osysharp.com/reference/ui/layout/).)

⚑ **`Button` is the one name the kit takes over.** The renderer also has a lower-level `Button` ATOM — `onClick`,
no label — and the kit's control shadows it. If you specifically want the primitive, name it `Osysharp.Button(…)`;
that `Osyrin.` prefix reaches any atom whose name a control shadows. You will rarely want it.

## Signature      {#signature}
```osy syntax
// nothing — the kit is in scope for every app.
// Optional, and only to PIN a version, in app.osy:
app MyApp { use Osysharp.Ui@2; }
```

The kit contributes three things to your app:

- **Controls** — styled components you reference by name.

  *Input and action*

  ⚑ **Every `value:` below is a two-way binding, and it takes one of exactly three things**: an assignable field of
  your component, a field of an **entity** row, or a `Binding<T>` you were handed. A field of a `class` value is
  **refused** — there is no row to write the edit through — so a list a person can EDIT is a list of entities.
  See [[ui-component#two-way]].

  | | |
  |---|---|
  | `Button(label, onPress, tone, size, disabled, canPress, whenDenied)` | The pressable action. One filled `Primary` per view; `Ghost` for the rest. `canPress:` takes a declared **policy** — the button greys out and says `whenDenied` when it does not hold for the caller. |
  | `Field(label, value, placeholder, hint, error, type, disabled)` | Labelled text input. `value` is a two-way binding, so typing writes straight to what you bound. The `label` also NAMES the input for a screen reader (and for `Ui.Fill`), and an `error` marks it invalid. `type:` is the kind of text a browser knows about — `"password"` masks what is typed, `"email"` / `"tel"` change the keyboard a phone offers. It is a CLOSED set, checked at compile time when written as a literal: `text`, `password`, `email`, `number`, `tel`, `url`, `search`, `date`, `time`, `datetime-local`, `month`, `week`, `color`, `range`. A checkbox, a radio group, a file picker and a submit button are their own controls (`Checkbox`, `Dropdown`, `Upload`, `Button`) rather than a `type:` — each keeps its state somewhere other than `value`, so the binding would be wired to nothing. |
  | `NumberField(label, value, min, max, unit, prefix, placeholder, hint, error, disabled)` | A whole number. Binds an `int` directly, so what is typed is stored AS a number — there is no string draft to parse. `min`/`max` are the field's RANGE: they bound the steppers and mark the box invalid the moment what is typed falls outside, so the reader sees it as they type rather than when the save is refused. `prefix` prints before the box (a currency symbol) and `unit` after it: `NumberField("How often", value: draft.EveryDays, min: 1, unit: "days")`. |
  | `DecimalField(label, value, min, max, unit, prefix, placeholder, hint, error, disabled)` | The same control over a `decimal` — money, a measurement, a rate: `DecimalField("Price", value: line.Price, min: 0m, prefix: "£")`. Two controls rather than one generic because a generic cannot do arithmetic over a number type it does not know. Both read and write their digits in the app's culture, so a reader under `app.DefaultCulture = "sv-SE"` sees and types `12,5` where one under `en-GB` sees and types `12.5` — see [Culture formatting — ToString(format, culture)](https://osysharp.com/reference/stdlib/culture-formatting/). |
  | `DatePicker(label, value, hint, error, disabled)` | A date field that opens a real month calendar — not `<input type=date>`, which cannot be styled and looks different in every browser. `‹`/`›` move a month, the chosen day is a filled circle, today carries a ring, and days from the neighbouring months are muted. The backdrop closes it: `DatePicker("Due", value: draft.DueOn)`. |
  | `TimePicker(label, value, minuteStep, use12Hour, hint, error, disabled)` | A time field that opens hour and minute columns. `minuteStep` is 5 by default, so quarter past is two clicks rather than fifteen; `use12Hour` switches the trigger to `h:mm tt`: `TimePicker("Opens", value: draft.OpensAt, minuteStep: 15)`. |
  | `DateTimePicker(label, value, minuteStep, hint, error, disabled)` | The calendar with a time strip under it, over one `DateTime` — a start, a deadline, a moment. Click the `14 : 30` strip to open the columns: `DateTimePicker("Starts", value: draft.StartsAt)`. For a calendar day with no time of day, use `DatePicker`. |
  | `Dropdown(label, value, options, placeholder, disabled)` | A styled select — `(label, value, …)` like every other field control here. Bind an **enum** and there is nothing else to say, because the options are its members: `Dropdown("Status", value: order.Status)`. Give it `options:` and it picks from **any rows** instead, with your own template per row: `Dropdown("Lead", value: project.Lead, options: people, placeholder: "Choose a lead…") { p => Avatar(p.Initials); Text(p.Name); }`. `label` is required and is what the control is announced and addressed by; only you know whether it means "Status" or "Order status". Clicking outside closes it, and so does Escape. |
  | `Option(label, selected)` | One row of a dropdown panel — the tick, and what a screen reader is told. Its content is the default slot, so `Option(selected: …) { Avatar(…); Text(…); }` draws your own row without losing the part that is not visible. Build your own picker from it. |
  | `Checkbox(label, value, onToggle, disabled)` | On/off in a form. The label is part of the hit target, and is also what it is announced as. |
  | `Switch(label, value, onToggle, disabled)` | On/off that takes effect **now** rather than on save. |
  | `ThemeToggle(glyph, label)` | Flips light/dark and remembers the choice. `glyph` is the button FACE (an emoji by default); `label` is what it is announced as — some apps say "Theme", some "Appearance". |

  *Structure*

  | | |
  |---|---|
  | `Card(title, subtitle)` | A raised surface. The block you wrap is its body; `slot actions { … }` puts controls in the header. |
  | `PageHead(title, subtitle)` | A page's heading row — title left, your actions right, hairline under. |
  | `Toolbar()` | A strip of controls with consistent spacing. Add a `Spacer()` to split left from right. |
  | `ListRow()` | One row of a list, with the between-rows hairline and rhythm. |
  | `Tabs()` · `Tab(label, selected, onPress, disabled)` | A tab strip. You own which tab is current. |
  | `Divider(vertical)` · `Spacer()` | A hairline, and a flex spacer that pushes what follows to the far end. |

  *Type roles* — say what the text **is**; the size follows.

  | | |
  |---|---|
  | `PageTitle(text)` · `CardTitle(text)` | A page's h1, and a section heading. |
  | `TextAreaField(label, value, placeholder, hint, error, rows, disabled)` | A labelled MULTI-LINE input — a reason, a note, a description. Everything `Field` does, plus `rows` for how tall it starts. Named for the kit's convention (`NumberField`, `DecimalField`); a control called `TextArea` would shadow the atom of that name. |
  | `TextLink(label, to, tone)` | A navigation link — `to` is the route, `label` is what the reader sees. The `Link` ATOM takes the HREF as its first positional, which is the shape HTML habits get backwards. |
  | `SectionLabel(text)` | The small uppercase label above a group. |
  | `FieldLabel(text)` · `Hint(text)` | A field's label, and secondary/help text. |
  | `Strong(text)` · `Metric(text)` | Emphasised body text, and a big stat number. |

  *Status and feedback*

  | | |
  |---|---|
  | `Badge(label, tone)` | A tinted status pill, sized to sit in a table cell. `label` is a **string**, so an enum needs `.Label` — `Badge(o.Status.Label, tone: Tone.Success)`. Its block replaces the label when you give it one, and inside a block the enum needs nothing: `Badge(tone: Tone.Success) { Text(o.Status); }`, because it is `Text` that turns an enum into its `[Label]` words. |
  | `Alert(message, title, tone)` | An inline message banner — a validation summary, a warning above a destructive form. |
  | `EmptyState(title, body)` | What a list looks like before it has anything in it. Wrap a call to action in it. |
  | `Avatar(initials, src, alt, size)` | Image when there is one, initials when there is not. |
  | `Spinner(size)` | Indeterminate work. Prefer a `skeleton { }` block for a first data read. |
  | `ProgressBar(percent, tone)` | Determinate progress, 0-100. Announces its value (`role="progressbar"` + `aria-valuenow`). |
  | `Skeleton(shape)` · `SkeletonText()` | Stand-ins for content still loading — put them in a `skeleton { }` block. |

  *Overlays* — render these behind an `if`.

  | | |
  |---|---|
  | `Dialog(title, onDismiss, subtitle)` | A modal. Brings its own scrim; `slot actions { … }` is the footer. |
  | `Menu()` · `MenuItem(label, onPress, tone, disabled)` | A floating menu panel and its rows. |
  | `Backdrop(onDismiss)` · `Scrim(onDismiss)` | Click-outside-to-close, invisible or dimmed. **This is the mechanism** behind every popover — pair it with your own panel. |
  | `Toast(message, tone)` | A transient message pinned to the corner. You own the timer. |

  *Data*

  | | |
  |---|---|
  | `DataGrid(rows, columns, rowSelected)` | A sortable, resizable, templatable table that becomes a card list on a narrow screen. Each column names its value with a **selector** over the row, so a rename is a compile error rather than a blank cell. A column's `Value` selector may read the page's own state as well as the row. Fill `slot <Column> { row => … }` to template a cell; set `Sortable = false` on a column that renders only through its template, so its header stops offering a sort that would reorder nothing. **Columns wider than the space scroll sideways** — the header row and the per-column filter row travel with the data, so a column never labels the wrong values, nothing is dropped, and the page itself never scrolls. The search box and `Columns` stay put above the scrolling table. |
  | `IconButton(onPress, label, tone, size, disabled, canPress, whenDenied)` | A square icon-only action — a row action, a toolbar affordance. The glyph is a slot, since icons are yours. **Pass `label`**: an icon has no text to be announced by, and only you know whether this one means "Save" or "Save draft". Reflects a policy exactly as `Button` does. |

  **The APP SHELL is not in this list.** A rail, a work area and open-document tabs ship as a sample you copy —
  `osy docs sample admin-shell` — rather than as controls you reference. A shell is one navigation model rather than
  a general one, and it is where an app's identity lives, so the source is yours from the first day.
  **[App shells](https://osysharp.com/reference/ui/shell/)** is the page for it.

  Each one is ordinary Osy# — atoms, style props and `Slot` — so you can read them to learn the house style, and
  fork any of them by dropping a same-named component in your own source.

  The kit's source ships inside the binary rather than in your project, so **`osy kit`** is how you read it:

  ```console
  $ osy kit              # every control's signature — the whole catalogue, one line each
  $ osy kit Field Card   # what those two are FOR, with a call you can copy
  $ osy kit --for "a labelled input"   # …when you cannot name the one you want
  $ osy kit Card         # Card's own declaration in full — the source you read to learn it, or fork
  $ osy kit Card --file  # …and the REST of the file it lives in: its file-mates, and the header's reasoning
  $ osy kit --atoms      # the renderer's own primitives, which need no `using`
  $ osy kit --tokens     # the starter theme's design tokens — every one, by group, with its value
  $ osy kit --json       # the same, for tools
  ```

  Both the listing and each signature are read out of the kit's own source, so they always describe the kit you
  actually have.

  This kit is not the only one. `osy kit` closes with the other kits the platform ships — what each is for, the
  controls it adds and the one line that turns it on — because a control you do not know exists is one you cannot
  choose. `osy kit --names` names the kit each set belongs to, and **`osy kit <Control>` finds a control in any of
  them**, telling you which kit it is in. They are listed from what the platform can actually resolve, not from
  what happens to be documented, so a kit with no reference page still appears.
- **Vocabularies** — the design-system enums a styled control reads: `Tone`, `Size`, `Step`. These are yours to
  extend (a brand adds `Tone.Success`).
- **A starter theme** — a complete token vocabulary, so the kit's own controls and your pages are written in names
  rather than pixels. It covers colour (`Bg`, `Surface`, `Border`, `Primary`, … with a light/dark mode map, so an app
  themes its whole page for free), radii, spacing, type (`Font`, `FontSize`, `FontWeight`), sizes (`ControlMd`,
  `IconMd`, `AvatarMd`, …), shadows, motion, z-layers and breakpoints. See [theme tokens](https://osysharp.com/reference/ui/theming/).

Those tokens are in scope for **your** components too, not just the kit's — `Bg = Surface`, `FontSize = Body`,
`H = ControlMd` resolve in a page you wrote, with no theme of your own. Declaring a theme is then how you *override*:
re-declare a token in the same group (`Radius { Md = "4px"; }`) and yours wins.

## Description    {#description}
A **kit control** is an ordinary component — you call it, pass its arguments, and it arg-checks exactly like a
component you wrote yourself. `Button` takes a `label`, an `onPress` handler, and `tone` / `size` variants:

```osy title="its tone and size variants" test app=ui-kit
[Page("/checkout")]
[AllowAnonymous]
[Render(CSR)]
component Checkout() {
  action Pay() { }
  render {
    Button("Pay now", onPress: Pay, tone: Tone.Primary, size: Size.Lg);
  }
}
```

### Greying out a button the user may not use — `canPress`   {#canpress}
A control that acts on data usually acts on data the caller may not be allowed to change. `canPress:` takes a
declared **policy** and greys the button out when it does not hold, with `whenDenied` as the reason — so the person
looking at it learns *why* instead of pressing and being refused:

```osy title="a button that greys out, and says why" test app=ui-kit-policy
[Principal]
entity Member { string Email; bool IsOwner = false; }

policy IsOwner => Member.Any(m => m.Email == user.Email && m.IsOwner);

[Page("/team")]
[Render(CSR)]
[AllowAnonymous]
component TeamPage() {
  action Invite() { }
  render {
    Button("Invite a member", onPress: Invite, tone: Tone.Primary,
           canPress: IsOwner, whenDenied: "Only an owner can invite members.");
  }
}
```

It only ever **reflects**: the server enforces the rule, and no button is a gate. `IconButton` takes the same pair.

The **`Tone`** and **`Size`** vocabularies are the variant dimensions a styled control exposes, and you can use them
in your own components too:

```osy title="the kit's vocabularies are yours to use" test app=ui-kit
[Composable] component Badge(Tone tone) {
  render { Text("badge"); }
}
```

### Forking a control     {#forking}
The kit is **forkable**: to change how a control looks or behaves, declare a component with the **same name** in
your own source. Your version **shadows** the kit's everywhere in your app — including inside other kit components
used by reference (a kit `Table` that renders `Button` renders *your* `Button`) — with no extra step:

```osy title="forking a kit control by shadowing its name" test app=ui-kit-fork
// This app's own Button — replaces the kit's for every reference in this app.
component Button(string label, Action onPress) {
  render { Pressable(label, onClick: onPress); }
}
```

A **`control`** of that name shadows the kit's too, and the same way — so an app whose grid is a foreign shim
(`control DataGrid<T> { … }`) keeps its own grid, and its call sites are checked against the shim it declares. One
name may only be declared once in your own source, though: a `component` and a `control` sharing a name is a compile
error naming both files, because between two of your own declarations there is nothing to prefer.

The fastest way to start from the kit's exact source is **`osy get`**, which vendors a control into `ui/lib/` as your
own source — the control's own declarations, verbatim, under a short header recording where they came from:

```console
$ osy get ui/hint        # one control → ui/lib/Hint.osy
$ osy get ui/*           # every control
```

**You get the control, not the file it lives in.** The kit groups components per file for the reader, and the two
groupings want opposite things from a fork: `Hint` shares a file with six other type roles you almost certainly do
not want to own, while a `DataGrid` is useless without its cells. So the unit is the **control**, and what travels
with it is reported:

```console
$ osy get ui/datagrid
✓ Forked DataGrid → ui/lib/DataGrid.osy
  ↳ also GridColumn (the class only DataGrid uses)
  ↳ also HeaderCell ([Internal] — part of DataGrid)
  ↳ also FixedCell ([Internal] — part of DataGrid)
  …
```

**Everything your fork does not take still resolves to the kit's.** A vendored `PageHead` goes on rendering the
kit's `PageTitle` and `Hint` — resolution is by name, and only the names you vendored are shadowed. That is what
makes a narrow fork safe: you own the one control you meant to change, and the rest keeps improving.

The vendored file is picked up by your `ui/**/*.osy` glob automatically, so it shadows the kit's with no manifest
edit. A forked control should stay **call-compatible** (keep its public parameters) so existing call sites — and kit
components that call it — keep resolving. Only your app is affected; the kit's original is untouched for every other
app.

## Examples       {#examples}
A small form using a kit `Button` and the layout vocabulary:

```osy title="a small page built from the kit" test app=ui-kit-contact
[Page("/contact")]
[AllowAnonymous]
[Render(CSR)]
component Contact() {
  string email = "";
  action Send() { }
  render {
    Stack(gap: 3, align: Align.Center) {
      Text("Get in touch");
      Input(value: email, placeholder: "you@example.com");
      Button("Send", onPress: Send, tone: Tone.Primary);
    }
  }
}
```

## See also       {#see-also}
- [App shells](https://osysharp.com/reference/ui/shell/) — the four app-shell arrangements, and how to build a `[Layout]` out of one.
- [component](https://osysharp.com/reference/ui/component/) — declaring your own components (kit controls are just components).
- [Pinning a kit version (using Ui@2)](https://osysharp.com/reference/ui/kit-versioning/) — pinning the kit's major with `using Ui@2;`.
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the design tokens the kit's controls and your theme share.
- [layout primitives](https://osysharp.com/reference/ui/layout/) — `Stack` / `Row` and the `gap` / `align` / `justify` vocabulary.


---

<!-- https://osysharp.com/reference/ui/pending/ -->

# Pending

> When a control's action waits on the server, the platform shows a busy spinner and disables the control — but only after a short delay, so a fast action never flashes one. It is automatic: you write nothing. To say something specific while one particular verb runs, read `save.Pending`; for a page-wide affordance (a top progress bar) read the `Pending` ambient. Tune the timing on `app.Ui`.

<!-- id: ui-pending · area: ui · stability: stable · html: https://osysharp.com/reference/ui/pending/ -->

## Summary        {#summary}
When a control's action waits on the server — an `onClick`/`onEnter` that calls a server function and hasn't come back
yet — the platform gives the user feedback automatically: after a short delay the control shows a **busy spinner** and
**disables itself** until the action finishes. You write nothing; every button that hits the server gets it.

The delay is deliberate. Most actions are fast, and a spinner that flashes for 40ms is worse than none — it reads as a
glitch. So the spinner appears only once an action has run longer than the threshold (200ms by default), and once shown
it stays visible for a short minimum so it can't blink off the instant the answer arrives. A fast action shows nothing.

Disabling the control while it is busy also blocks an accidental double-submit — a second click on a half-second "Save"
does nothing rather than firing the action twice.

When you want to say something **specific** while one particular verb runs — a button that reads "Reading…" rather than
just spinning — read that verb's own flag: `Read.Pending` is true while an invocation of `Read` is in flight. It follows
the one action it names, so a page that saves while a read is running does not relabel the read button.

For a **page-wide** busy affordance — a top progress bar, a dimmed overlay — read the `Pending` ambient: `Pending.Any`
is true while any action is in flight, `Pending.Count` is how many. That is the one thing you author; the per-control
spinner needs no code at all.

Three grains, and picking the wrong one is the usual mistake:

| You want | Read | Grain |
|---|---|---|
| A spinner on the control that was pressed | *nothing* — it is automatic | that control |
| A label or field that speaks for ONE verb | `save.Pending` | that action |
| A top bar / overlay for the whole page | `Pending.Any` · `Pending.Count` | the page |

## Signature      {#signature}
```osy syntax
// Automatic — no code. A control whose action suspends on the server shows a delayed spinner + disables itself.

save.Pending     // true while an invocation of THIS action is in flight (a bool) — per-verb
Pending.Any      // true while at least one action is in flight (a bool) — for your own global busy surface
Pending.Count    // how many actions are in flight right now (an int)

// Tune the timing (and, later, supply your own spinner) on app.Ui:
app.Ui = new AppUi {
  PendingDelayMs   = 200,   // show the spinner only after an action runs this long (0 → the 200ms default)
  PendingMinShowMs = 300,   // once shown, keep it at least this long so it can't blink off (0 → the 300ms default)
};
```

## Description    {#description}
The busy affordance is **mechanism, not chrome**. The platform draws a plain spinner (`.osy-spinner`) beside the
control's label and applies the ordinary disabled styling; an app that wants a different look styles it in its own CSS,
the same way it styles any other control state.

It is entirely **client-side**. The first server-rendered paint is never busy (there is nothing pending yet), so a page
reading `Pending.Any` renders its idle state on the server and lights up only once the user starts something.

Which events count: the spinner is for **activation** events — `onClick`, `onEnter`, `onSubmit`, and an `Upload`'s
`onUploaded`. Typing and focus events (`onInput`, `onChange`, `onBlur`) do not spin the field — a keystroke that runs a
check should not make the input look busy. The `Pending` ambient counts every in-flight action regardless.

`Pending.Any`/`Pending.Count` update the **instant** an action begins and settles — the ambient is the raw in-flight
count, not the delayed spinner. So a top bar bound to `Pending.Any` appears immediately; if you want it to respect the
same "don't flash" delay, gate it on your own timer.

`<verb>.Pending` is available on every `action` and `method` a component declares, and reads as an ordinary `bool`
anywhere an expression is legal — a ternary, a prop value, an `if`. Like the ambient it flips the **instant** the action
begins, with no delay: it is what you reach for when you would rather say "Reading…" than show a generic spinner, and
you want that word to appear at once.

It counts **every** invocation of that action, not only the one a control activated — an action called from another
action is still running, and a label claiming otherwise would be wrong in the one case you most want to explain. It also
clears when an action **fails**: the work is over, it just went badly, and a button stuck reading "Reading…" forever is
worse than one that goes back to normal while your failure surface says what happened.

Actions are serialised per page, so an action can sit queued behind another. `.Pending` is true for that wait too — from
the user's side the work has already started, and the label is what tells them so.

A misspelling is a compile error naming what exists (`save.Pendign` → *"a verb has no member 'Pendign' (Pending)"*),
rather than a value that silently reads as nothing.

`PendingDelayMs` / `PendingMinShowMs` are app-wide timing, read from `app.Ui`. Leave them out (or set 0) and the
platform defaults apply. A negative value is a compile error.

## Examples       {#examples}
The common case is **nothing** — the spinner is automatic:
```osy title="no pending code at all — the spinner is automatic" syntax
component InvitePage() {
  action Accept() { var t = AcceptInvite(token, email, password); Session.SignIn(t); }
  render {
    // No pending code. `Accept` calls a server function, so this button spins + disables while it runs.
    Pressable(onClick: Accept) { Text("Accept invitation"); }
  }
}
```

`AcceptInvite` here is a server function reached by a signed-out visitor, so a working version of this also needs it
marked `[AuthMethod]` and wired into `app.AuthBootstrap` — see [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/). The point above is only that
the spinner costs no code either way.

A button that says what it is doing, using the action's own flag:
```osy title="a label that says what this one action is doing" syntax
component ExpenseLineRow(ExpenseLine line) {
  action Read() { ReadReceipt(line); }
  render {
    Pressable(onClick: Read) {
      // Follows `Read` alone — another action finishing elsewhere on the page leaves this label untouched.
      Text(Read.Pending ? "Reading…" : "Read receipt");
    }
  }
}
```

The same flag on a control that takes a `busy` prop, and to keep a form from being resubmitted:
```osy title="the busy prop, and blocking a second submit" syntax
component LoginCard() {
  action SignIn() { Session.SignIn(Authenticate(email, password)); }
  render {
    Button("Sign in", onPress: SignIn, busy: SignIn.Pending, disabled: email == "");
  }
}
```

A page-wide top progress bar, using the `Pending` ambient:
```osy title="a page-wide progress bar" test app=ui-pending
theme App { Colors { Accent = "#0077B6"; } }   // `Accent` is your own token, not a platform one

[Layout]
component Shell() {
  render {
    if (Pending.Any) { Box(h: "2px", bg: Colors.Accent, w: "100%"); }   // a thin bar while anything is in flight
    Outlet();
  }
}
```

Tuning the timing for a whole app:
```osy title="app-wide spinner timing" test app=ui-pending
app.Ui = new AppUi {
  PendingDelayMs   = 120,   // this app's actions are usually instant — show the spinner sooner when they aren't
  PendingMinShowMs = 400,   // …but once it shows, hold it a beat longer
};
```

## See also       {#see-also}
- [UI surfaces (app.Ui)](https://osysharp.com/reference/config/ui/) — the `app.Ui` block that tunes the timing (and nominates the app's system surfaces).
- [Connection](https://osysharp.com/reference/ui/connection/) — the sibling surface for the server-*dropped* case (a full connection-loss overlay).
- [component](https://osysharp.com/reference/ui/component/) — the components and controls the spinner attaches to.


---

<!-- https://osysharp.com/reference/ui/kit-versioning/ -->

# Pinning a kit version (using Ui@2)

> A kit like `Ui` is versioned independently of the platform, so you pin the major you build against with `using Ui@2;`. The number is a stability floor that never auto-crosses the next major. Platform capabilities (`Storage.Blob`, `Content.Markdown`, …) are version-neutral — they ride the platform version — so pinning one is a compile error.

<!-- id: ui-kit-versioning · area: ui · stability: stable · html: https://osysharp.com/reference/ui/kit-versioning/ -->

## Summary        {#summary}
A **kit** (like [the UI kit](https://osysharp.com/reference/ui/kit/)) is a library of components, vocabularies, and a theme that is versioned
**independently of the platform**. You pin the major version your app builds against by adding an `@major`
suffix to its `using`:

```osy syntax
using Ui@2;
```

`@2` is a **stability floor**: your app resolves to some `2.x`, and never silently jumps to a breaking `3.x`.
The version applies only to **kits** — the forkable, independently-shipped libraries. Ordinary **platform
capabilities** (`Storage.Blob`, `Memory`, `Content.Markdown`, `Observability`, …) are
version-neutral, so pinning one is rejected.

## Signature      {#signature}
```osy syntax
using Ui@2;        // any 2.x       — a major floor
using Ui@2.3;      // any 2.x ≥ 2.3 — a minor floor
using Ui@2.4.1;    // exactly 2.4.1 — an exact pin
```

The suffix is `@major[.minor[.patch]]`. A bare `@` with no number is an error.

## Description    {#description}
Kits and platform capabilities differ in one decisive way — **can you resolve an old or new version
independently?**

- A **kit** is composition (components, variant recipes, vocabulary members, a theme) shipped on its own
  cadence. An older or newer major exists as its own set of source, so pinning a version is meaningful.
- A **platform capability** is provided by the platform you run. There is no separate "version 2" of
  `Storage.Blob` to resolve to — it is whatever the running platform provides. The **platform version is its
  version**, so pinning would be a fiction.

Because of that, a version pin is legal **only on a kit**. Pinning a platform capability is a compile error
that names the fix:

```osy syntax
using Storage.Blob@2;
// error VERSION_ON_PLATFORM_CAPABILITY: 'using Storage.Blob@2;' pins a version on a platform capability,
// which is version-neutral (it rides the platform version). Drop the '@2' — a version pin is only meaningful
// on a kit (e.g. `using Ui@2;`).
```

If your app needs a newer platform (for a capability feature that only a newer platform provides), that is an
**app-level** requirement, not a per-capability pin. Requiring a minimum platform is expressed at the app level,
not by writing `@version` on a capability.

## Which exact version did I get? — `osyrin.lock`   {#lockfile}
Your `using Ui@2;` declares **intent** (a stability floor). The exact resolved version is recorded in
**`osyrin.lock`** so a build is reproducible across machines and matches what the server compiles — the same split
as `package.json` vs `package-lock.json`.

Run `osyrin lock` to resolve your pins and write the file:

```console
$ osyrin lock
✓ Wrote osyrin.lock (2 pins)
  Ui              2.0.0 (bundled)
  Storage.Blob    platform capability
```

Each kit entry records the resolved `version`, a `hash` of the resolved source (the integrity check — an edited or
stale local kit copy is caught when you `osy compile`), the `minPlatform` the kit needs, the `source` it came
from, and the `constraint` you declared. A platform capability is recorded without a version (it rides the
platform). The happy path is **offline**: a default pin resolves to the kit **bundled** with your platform — no
network. Pinning a version the bundled kit can't satisfy is a clear error, not a silent mismatch:

```console
$ osyrin lock          # with `using Ui@3;` but only 2.x bundled
ERROR  KIT_VERSION_UNAVAILABLE  'using Ui@3;' requests a version the bundled kit (2.0.0) does not satisfy…
```

## Updating & reconciling forks   {#updating}
Stay on your major but pick up the newest compatible kit with **`osyrin update`** — it re-resolves within the
declared major (never crossing to the next) and rewrites `osyrin.lock`:

```console
$ osyrin update ui
  Ui  2.0.0 → 2.1.0
✓ Updated 1 kit in osyrin.lock
```

After an update, **`osy diff`** shows how your forks in `ui/lib/` differ from the kit's new source, so you can
reconcile them (shadcn-style):

```console
$ osy diff ui/button
≠ Button (your fork vs kit default)
  + // my customization
```

A fork that matches the kit exactly is flagged as safe to drop.

Each vendored file records the kit version it was taken from, so `diff` can tell you the other thing a text
comparison cannot — that the **kit itself** has moved on since you forked:

```console
$ osy diff ui/button
! Button — forked from Osysharp.Ui 2.0.0, the kit is now 2.1.0. What follows includes the kit's own changes since.
```

That line is the real cost of a fork. The code is yours either way; what you give up is receiving improvements to
it, and this is how you find out what you are missing.

## Examples       {#examples}
Pin the UI kit's major and use a platform capability version-neutrally in the same manifest:

```osy title="pinning a kit, and a version-neutral capability" test app=ui-kit-versioning
app Shop {
  model "model/**/*.osy";
  use Osysharp.Ui@2;         // kit — pinned to major 2
  use Osysharp.Storage;      // platform capability — version-neutral, no @
}
```

⚠ A version pin belongs on the manifest's **`use`**, never on a source file's `using`. `using` is a C# import and
carries no version — `using Osysharp.Ui@2;` is refused with a message saying exactly this.

## See also       {#see-also}
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — what the UI kit contributes and how to fork a control.
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the design tokens a kit's controls share with your theme.
- [component](https://osysharp.com/reference/ui/component/) — declaring your own components (kit controls are just components).


---

<!-- https://osysharp.com/reference/ui/shell-rail/ -->

# RailShell

> An app shell built around a permanently narrow icon rail, in the shape Slack, Linear and Discord converge on. Every row reveals its label on hover and on keyboard focus, groups open a flyout beside the rail, and the signed-in person sits at the rail's foot rather than in the top bar.

<!-- id: ui-shell-rail · area: ui · stability: stable · html: https://osysharp.com/reference/ui/shell-rail/ -->

## Summary        {#summary}

`RailShell` is a **permanently narrow** icon rail — 60px, at every pointer width, with no collapse control. Reach
for it when the canvas is the product and the navigation should take as little of it as possible, and when the app
has few enough top-level sections that a glyph each is genuinely legible.

```osy title="the same chrome, arranged as a rail" test app=ui-shell-rail
[Layout]
[AllowAnonymous]
component RailLayout() {
  action Palette() { }
  action SignOut() { Session.SignOut(); }

  render {
    RailShell(new AppChrome {
      Product = "Expensely",
      Home = "/",
      User = new ShellUser {
        Name = "Olivia Rhye", Secondary = "Finance manager", Initials = "OR",
        Menu = [ new MenuAction { Label = "Sign out", Icon = Icons.Logout, OnPress = SignOut, Tone = Tone.Danger } ],
      },
      Nav = [
        new NavItem { Label = "Overview", To = "/", Icon = Icons.Home, Exact = true },
        new NavItem { Label = "Approvals", To = "/approvals", Icon = Icons.CheckCircle, Badge = "12", BadgeTone = Tone.Warning },
        new NavItem { Kind = NavKind.Section, Label = "Administration", Children = [
          new NavItem { Label = "Settings", To = "/settings", Icon = Icons.Gear, Children = [
            new NavItem { Label = "People", To = "/settings/people", Icon = Icons.Users },
          ] },
        ] },
      ],
    }) {
      Outlet(retain: 8);
      slot search { ShellSearch("Search", onPress: Palette); }
    }
  }
}

[Page("/")] [Layout(RailLayout)] [Title("Overview")] [Render(CSR)] [AllowAnonymous]
component ROverview() { render { PageHead("Overview"); Card("This month") { Text("Nothing needs you."); } } }

[Page("/approvals")] [Layout(RailLayout)] [Title("Approvals")] [Render(CSR)] [AllowAnonymous]
component RApprovals() { render { PageHead("Approvals"); } }

[Page("/settings")] [Layout(RailLayout)] [Title("Settings")] [Render(CSR)] [AllowAnonymous]
component RSettings() { render { PageHead("Settings"); } }

[Page("/settings/people")] [Layout(RailLayout)] [Title("People")] [Render(CSR)] [AllowAnonymous]
component RPeople() { render { PageHead("People"); } }
```

## Signature      {#signature}

```osy syntax
RailShell(AppChrome chrome) {
  Outlet(retain: 8);          // the routed page
  slot search   { … }         // the top bar
  slot actions  { … }         // the top bar, right of search
  slot railFoot { … }         // the phone DRAWER only — a 60px rail cannot hold a card
  slot aside    { … }         // beside the page at 1100+, under it below that
  slot primary  { … }         // a task's commit bar — `ShellTaskBar`. See [FocusedShell](https://osysharp.com/reference/ui/shell-focused/)
}
```

## Description    {#description}

### How is this not the sidebar collapsed?   {#versus-sidebar}

`SidebarShell` narrows to an icon strip as a **state** — the reader chooses it, the wide band un-chooses it, and
while it is narrow a row shows a glyph and nothing else. `RailShell` is narrow **always**, and is built around that
constraint rather than tolerating it. Three things differ, and all three are mechanism:

- **Every row carries its label**, as a flyout on hover *and* on keyboard focus. The sidebar's collapsed rail has
  none, which makes an icon-only nav a memory test — the single biggest reason narrow rails have a bad reputation.
- **The identity lives at the rail's foot**, not in the top bar. That is not decoration: it frees the entire top bar
  for page context, which is what lets this arrangement give a page a genuinely wide, uncluttered header.
- **It expands into a panel that FLOATS.** Press "Expand navigation" at the rail's foot and the 60px rail widens
  into a labelled panel *over* the page. The content column never reflows, because the 60px column stays exactly
  where it was and the panel is a layer you dismiss rather than a resize you undo.

⛔ **Floating rather than pushing is what keeps these two shells two shells.** `SidebarShell` already expands by
**changing its width**, with the page reflowing each time; that is its model and a good one for a shell whose nav
is the app's primary structure. A rail that did the same would be `SidebarShell` with a smaller collapsed width and
no reason to exist.

### Expanding the rail   {#expand}

The control sits at the **foot of the rail, above the identity** — chrome *about* the rail belongs with the rail's
other chrome, not among the destinations (a row that is not a place) and not crowding a 60px brand tile.

It is a real button with a changing accessible name — `"Expand navigation"` / `"Collapse navigation"` — and it
carries `expanded:`, so assistive technology is told which state it is in and a test can drive it. An expansion only
a mouse can reach is not an expansion.

```osy title="driving the rail's expansion in a test" syntax
Assert.Hidden("Approvals", within: "Main navigation");   // narrow: glyphs only, the label is a hover flyout
Ui.Click("Expand navigation");
Assert.Visible("Approvals", within: "Main navigation");  // the panel labels every row in place
Ui.Click("Collapse navigation");
```

While expanded, every row shows its label and badge in the row itself, `railFoot` appears (a panel has the width for
a card), and the identity chip shows the person's name rather than only their mark. Following any row collapses the
panel again — the panel covers the page, and the page is what you just asked for.

⚠ **Expansion is a pointer affordance only.** On a phone the drawer already shows every label, so there is nothing
to expand into and the control is not drawn.

### How a label appears with no JavaScript   {#tips}

The tip is an **ancestor-conditioned variant**: a descendant's `base` block names an ancestor component and a
pseudo-state, and it lowers to a plain CSS descendant rule.

```osy title="the rule that reveals a rail label" syntax
variants {
  base { Display = Display.None; Position = Position.Absolute; Left = "100%";
         inside RailNavLink.Hover { Display = Display.Flex; }
         inside RailNavLink.Focus { Display = Display.Flex; } }
}
```

⚠ **The `.Focus` twin is not optional.** A rail whose labels exist only under a pointer is unusable from the
keyboard. And `:focus` matches only the **focusable element itself**, so the ancestor named here has to be the
`Link` — a wrapper element as the component's root would make the keyboard half silently dead.

⚠ **`Display.None`, never `Opacity = 0`.** A merely transparent tip keeps its text in the layout and in
`textContent`, so every rail label would read as on-screen to `Assert.Visible` whether or not anybody could see it.

### What does a group do?   {#groups}

A group's children open a **flyout beside the rail**, pinned by a **click** — not by hover. A hover-only disclosure
does not exist on a touch screen, so its children would be unreachable, which is the same as not shipping them.
Hover and focus reveal the row's *label*, which is purely informational and safe to leave to the pointer.

A group carrying a `To` **navigates as well as disclosing**: a link with a thin chevron strip down its trailing
edge, named `"Settings sub-pages"` so the two targets in one row do not answer to the same words. Anything nested
deeper than the flyout draws is flattened into it rather than dropped.

⚑ **This is the one shape a rail keeps that a tab strip does not**, and the difference is the model. A rail row is a
row in a *list* — disclosing under or beside it is what a list does, and the flyout exists because 60px has no room
for the children's names. A tab is a *destination*, so [TabbedShell](https://osysharp.com/reference/ui/shell-tabbed/) refuses the same mechanism and puts a
section's areas in a secondary strip instead. Expanding the rail closes any pinned flyout, because the panel shows
those names in place and the same information twice is worse than once.

### What changes on a phone?   {#bands}

The rail becomes a **labelled off-canvas drawer** opened from the top bar — a drawer has the width for words, so
the rows are labelled there rather than icon-only, and `railFoot` appears because a drawer can hold a card.

⚑ This is close to the sidebar's compact band **on purpose**. On a phone a left-nav app *is* a drawer; inventing a
difference in order to look different would be a worse design. What the arrangement keeps is its own identity
placement — the person is at the drawer's foot, not in the top bar.

## Examples       {#examples}

```osy title="driving the rail in a test" syntax
// The rail is narrow — the claim that distinguishes the arrangement, and the only one geometry can check.
Assert.Narrower("Main navigation", "Page");
Assert.LeftOf("Main navigation", "Page");

// A group pins its children BESIDE the rail rather than over it.
Ui.Click("Settings sub-pages");
Assert.Visible("People");
Assert.RightOf("People", "Overview");
```

## Notes          {#notes}

⚠ **A hover-revealed affordance cannot be driven by a test today** — there is no `Ui.Hover` verb, so the tip is
covered by a screenshot rather than an assertion. Everything reachable by a click or by focus is asserted normally.

⚠ **`railFoot` is hidden in the docked bands.** A 60px rail cannot hold a card, and a clipped card reads as a
rendering fault where its absence reads as a narrow rail.

## See also       {#see-also}

- [App shells](https://osysharp.com/reference/ui/shell/) — the shared `AppChrome` contract every arrangement reads
- [TabbedShell](https://osysharp.com/reference/ui/shell-tabbed/) — top tabs, and a bottom bar on a phone
- [FocusedShell](https://osysharp.com/reference/ui/shell-focused/) — one task, no navigation at all
- [style props](https://osysharp.com/reference/ui/styling/) — the style-prop vocabulary, including `inside <Component>.<State>`
- [accessibility](https://osysharp.com/reference/ui/accessibility/) — landmarks, `role:`, `current:` and the naming props


---

<!-- https://osysharp.com/reference/ui/svg-assets/ -->

# SVG assets

> Drop a `.svg` into `model/art/` and render it with `Svg(Art.Hexgrid)`. Unlike an icon, an asset keeps its own colours, gradients and patterns — it is the illustration, background or multi-colour logo counterpart to the single-colour `Icon`. The name is checked at compile time, and the asset is placed with ordinary style props.

<!-- id: ui-svg-assets · area: ui · stability: stable · html: https://osysharp.com/reference/ui/svg-assets/ -->

## Summary        {#summary}
An SVG asset is a **file in your app**, not data. Put a `.svg` in `model/art/` and it becomes part of your app's
vocabulary:

```text
model/
  art/
    hexgrid.svg
    hero.svg
    logo-full.svg
```

```osy title="an asset behind a page" test app=arcade
[Composable]
component Splash() {
  render {
    Box(position: Position.Relative, minH: "100vh") {
      Svg(Art.Hexgrid, position: Position.Absolute, inset: 0, z: "-1");   // a full-bleed background
      Text("Welcome");
    }
  }
}
```

This compiles against the [drop-ship-order](#see-also) sample, whose `art/` folder really does contain `hexgrid.svg`
— the name is checked against the files the app ships.

Adding an asset is dropping a file in. There is nothing to register and nothing to import.

## Signature      {#signature}
```osy syntax
Svg(Art.Hexgrid) · Svg(Art.Hexgrid, position: Position.Absolute, inset: 0) — a named SVG asset, placed with style props
```

## Description    {#description}

### Asset or icon?   {#vs-icon}
Reach for an **asset** when the artwork carries its own colours — an illustration, a background pattern, a hero
image, a full-colour logo. Reach for an [icon](https://osysharp.com/reference/ui/icons/) when it's a single-colour glyph that should follow the
surrounding text colour.

| | `Icon(name)` | `Svg(name)` |
|---|---|---|
| Lives in | `model/icons/` | `model/art/` |
| Colour | recoloured to the current text colour | **its own** colours, gradients, `<pattern>`s |
| Sizing | an em square (`size:`) | placed with style props (`w:`/`h:`/`position:`/…) |
| For | UI glyphs | illustrations, backgrounds, multi-colour logos |

### The name is checked   {#names}
`Svg(Art.Hexgrid)` names the asset by a **bare identifier**, checked against the assets your app actually declares. A
typo is a compile error with a suggestion:

```text
unknown SVG asset 'hexgrd' (declared assets: hero, hexgrid, logo). Did you mean 'hexgrid'?
```

Because the name is an identifier, an asset's **file name must be one too** — `logo_full.svg`, not
`logo-full.svg`. A kebab-case file is rejected with the rename to make.

The name is never an expression. A local variable called `hexgrid` does **not** change what `Svg(Art.Hexgrid)` means —
the asset vocabulary always wins. An asset chosen at runtime is a *content* concern, not chrome: use `Image(src)`
for that.

### Sizing and placing an asset — ordinary style props   {#placement}
An asset keeps its own colours, so there is no `size:`/`color:`. Instead it takes the ordinary
[style props](https://osysharp.com/reference/ui/styling/), so you place it like any other element — a sized inline logo, or a full-bleed
background behind a card:

```osy title="placing one with ordinary style props" test app=arcade
[Composable]
component Wordmark() {
  render { Svg(Art.LogoFull, w: 140); }            // an inline, fixed-width logo
}

[Composable]
component Patterned() {
  render {
    Box(position: Position.Relative) {
      Svg(Art.Hexgrid, position: Position.Absolute, inset: 0, z: "-1");   // tiles behind the box's content
      Slot;
    }
  }
}
```

By default an asset fills the box you give it, so a full-bleed background is `position: Position.Absolute; inset: 0` on a
`position: Position.Relative` parent, and a fixed-size asset is just `w:`/`h:`. Anything other than the asset name and style
props is a compile error.

### What an asset may contain   {#contents}
An asset is drawing: shapes, groups, and the paint machinery that gives it colour — `linearGradient`,
`radialGradient`, `pattern`, `clipPath`, `mask`, and a `<defs>` block, with in-document `fill="url(#…)"` references
to them. Its colours are kept exactly as drawn.

Anything that could **run, load, or reach outside the file** is a compile error, naming the file:

```text
'evil.svg' contains a <script> element — scripting, styling, embedding, external
references and animation are not allowed in an SVG asset.
```

That covers `<script>`, `<style>`, `<image>`, `<use>`, `<a>`, animation elements, any `on…` handler, and any URL
that leaves the document — an external or `data:` image, a `javascript:` link, a `url(https://…)`. A `url(#id)`
that points **inside the same asset** (a gradient or pattern fill) is fine; each asset's ids are kept separate, so
two assets that happen to use the same id never collide. An SVG is a place scripts can hide, and your assets are
placed directly into your pages — so the rule is an allow-list, and it is not negotiable.

### Assets from a UI kit   {#kits}
A kit's assets land in your app tree alongside your own and are picked up the same way. Two files claiming the same
name is an error naming both, so an asset always resolves to exactly one file.

### How assets are delivered — inline, on the first byte   {#delivery}
A server-rendered page paints its assets inline on the very first byte — no request, no flash. Change an asset and
the delivered copy changes with it; leave it alone and browsers keep the copy they already have.

Custom glob, if `model/art/` doesn't suit you:

```osy syntax
app Admin {
  model "model/**/*.osy";
  svg "assets/art/*.svg";
}
```

### Passing an asset around — `Art` is a type   {#as-a-value}
`Art` is a type, so an SVG asset can be a component parameter, a return value or a stored field:

```osy syntax
[Composable]
component Badge(string label, Art art) {
  render {
    Stack {
      Svg(art, w: 120);
      Text(label);
    }
  }
}

// at the call site
Badge("Grid", Art.Hexgrid);
```

The vocabulary is built from the files themselves, so there is nothing to declare — and an app that ships no
`model/art/` files simply has no `Art` type yet, which the compiler says in those words.

## See also   {#see-also}
- [icons](https://osysharp.com/reference/ui/icons/) — the single-colour glyph counterpart, recoloured to the current text colour.
- [style props](https://osysharp.com/reference/ui/styling/) — the `position`/`inset`/`w`/`h` props that place an asset.
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the tokens the surrounding layout resolves against.


---

<!-- https://osysharp.com/reference/ui/current-user/ -->

# Session.CurrentUser

> `Session.CurrentUser` is the person the app is being shown to, as your own `[Principal]` entity. Read it in a member, in a render expression, or straight inside an action — the compiler puts the read where it belongs, so `Owner = Session.CurrentUser` in a Save button means what it looks like it means. A single scalar of it (`Session.CurrentUser.Email`, `.Id`) costs no round trip at all. It is null for a visitor who has not signed in.

<!-- id: ui-current-user · area: ui · stability: stable · html: https://osysharp.com/reference/ui/current-user/ -->

## Signature      {#signature}
```osy syntax
Session.CurrentUser          // your [Principal] entity — the signed-in person. Null when nobody is.
Session.CurrentUser.Email    // any SCALAR of theirs — resolved in place, no round trip
Session.SignOut()            // the other end of the session — drops the ticket, back to the login page
```

Available in every component, with no `using`. Requires the app to declare a `[Principal]` entity.

## Summary        {#summary}
`Session.CurrentUser` is the signed-in principal — a row of whatever entity your app marked `[Principal]`, with the
fields you declared on it. It is **null when nobody is signed in**, which is the honest answer and the reason a page
that needs a user should say so with routing rather than by checking here.

You can read it in any of the three places a component holds a value, and you do not have to know which:

- as a **member** — `var me = Session.CurrentUser;` — the whole row, fetched when the page loads;
- in a **render expression** — `Text(Session.CurrentUser.Email)`;
- inside an **action** — `new Order { Reference = reference, Owner = Session.CurrentUser };`

The third one is the one that used to need a workaround. It does not any more: the compiler hoists the read onto the
component and the action closes over it, which is precisely what you would have written by hand.

## Description    {#description}

### Reading a single field costs nothing        {#scalars}
A scalar of the principal — `Session.CurrentUser.Email`, `.Id`, or any column you declared — resolves **in place, on
whichever side is asking**, with no round trip. The browser reads it from the bag the server sent at boot, built
server-side, so a field your security rules mask reads null in the browser exactly as it does on the server. Use it
freely in a render expression, a filter, or a field default:

```osy syntax
Text($"Signed in as {Session.CurrentUser.Email}");
live var mine = Order.Where(o => o.Owner.Id == Session.CurrentUser.Id);
```

Reaching **through** a reference (`Session.CurrentUser.Manager.Name`) is a different question — the browser holds the
manager as an id, not as a row — so that stays a server read and belongs on a member.

### The whole row, in an action        {#in-an-action}
An action runs in the browser, and the browser cannot run a database read in the middle of one. So when an action
mentions `Session.CurrentUser`, the compiler lifts the read onto the component as an ordinary fetched member and the
action reads that. Several actions on one page share one fetch.

```osy test app=ui-current-user
[Principal] entity User {
  [Required, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Order {
  [Required, MaxLength(50)] string Reference;
  User Owner;
  security { allow read, create when IsAuthenticated; }
}

[Page("/orders/new")]
[Render(CSR)]
component NewOrder() {
  string reference = "";

  action Save() {
    new Order { Reference = reference, Owner = Session.CurrentUser };
    UnitOfWork.Commit();
  }

  render {
    Stack {
      Input(value: reference, placeholder: "Reference");
      Pressable(onClick: Save) { Text("Save"); }
    }
  }
}
```

⚠ **Other reads behave differently inside an action, and it is worth knowing which.** An ordinary read —
`Order.Where(…)` — is **not yet available inside an action**: bind it to a member and read the member instead. The
browser has no database, so a read in the middle of an action means a round trip at that moment; the compiler does not
yet arrange one. `Session.CurrentUser` needs no such arrangement — its value is fixed for as long as the page is open
(signing in or out reloads the app), so it is fetched with the page and simply read.

### It is who is being SHOWN the page, not who wrote the row        {#versus-audit}
Every entity is audited automatically, and `CreatedBy` on a saved row is stamped **by the server** from the request's
principal. Your own field (`Owner` above) is the app's view; the audit column is the platform's, and no app code can
write it. When you want proof of who did something, read the audit column. When you want a relationship you control —
who a task is assigned to, whose basket this is — declare your own reference and set it.

### Nobody is signed in        {#anonymous}
`Session.CurrentUser` is null for an anonymous visitor. Do not use that null as a gate: a page that requires a user
should require one at the route (routed components are protected unless they say `[AllowAnonymous]`), so the page is
never rendered for someone who is not there. If you want a name for work done *before* sign-in, that is
[Visitor](https://osysharp.com/reference/ui/visitor/).

## See also       {#see-also}
- [Visitor](https://osysharp.com/reference/ui/visitor/) — the anonymous twin, for work that begins before there is a user
- [component](https://osysharp.com/reference/ui/component/) — members, actions, and where each kind of value lives
- [[security-auth-bootstrap#sign-out]] — `Session.SignOut()`, the verb that ENDS the session this page reads
- [principal predicates (IsAuthenticated / IsAnonymous) and open reads](https://osysharp.com/reference/security/principal-predicates/) — the predicates that read the same principal in a `security { }` block


---

<!-- https://osysharp.com/reference/ui/image/ -->

# Showing a picture on a page

> `Image` is the element that shows a picture, and it addresses one of two ways. `src` takes a URL — for a file in the app's own path store, `File.Url(path)` builds it. `fileAsset` takes the id of a stored `FileAsset` row, which has no path at all: the renderer asks the platform for a short-lived address when it draws, so a private photograph is shown without the app minting or handling a URL. Exactly one of the two, always.

<!-- id: ui-image · area: ui · stability: stable · html: https://osysharp.com/reference/ui/image/ -->

## Summary        {#summary}
`Image` is a renderer primitive — no `using`, nothing to install. It renders one picture, and the argument you give
it says **where the picture is**:

| you have | write | what it is |
|---|---|---|
| a URL, or a path in the app's own file store | `Image(src: File.Url(item.ImagePath))` | a plain address the browser fetches |
| a `FileAsset` row — what `File.Create` and `Image.Thumbnail` answer with | `Image(fileAsset: item.Photo.Id)` | the platform works out the address |

**Give it one or the other, never both and never neither** — an `Image` shows one picture, and an element with no
address draws a broken-image glyph rather than nothing, which reads as a failure of the app.

## Signature      {#signature}
```osy syntax
Image("/logo.png")                                     // the first positional IS `src`
Image(src: File.Url(item.ImagePath), alt: "A wheel")   // a file in the app's own PATH store
Image(fileAsset: item.Photo.Id, alt: item.Photo.AltText)   // a stored FileAsset ROW
```

Everything else an `Image` takes is the ambient vocabulary every element has — the style props (`w`, `h`, `rounded`,
`objectFit`), the accessibility props, the event props. `osy kit atoms Image` prints the lot.

## Description    {#description}

### Why is there a second way at all?   {#two-addresses}
Because the platform stores files two ways, and only one of them has a path.

A **path-keyed** file lives at an app-relative address you chose — `public/logo.png` — and [File.Url](https://osysharp.com/reference/storage/file-url/) turns
that into a URL. That is what the [upload](https://osysharp.com/reference/ui/upload/) control produces, and `src` is how you show it.

A **`FileAsset`** is a *row*. It has an id, a name, a MIME type and its own security, and it has **no path** — which
is why `File.Url`/`File.SignedUrl`, whose argument is a path, cannot address one. It is what `File.Create` answers
with, what `Image.Thumbnail`/`Resize`/`Convert` answer with (see [Image.Thumbnail, Resize and Convert (a stored image, transformed)](https://osysharp.com/reference/storage/images/)), and what a `FileAsset` field on
your entity holds. `fileAsset:` is how you show one.

### Why the row, and not a URL   {#why-the-row}
The address a browser can fetch a **non-public** `FileAsset` from is a short-lived signed grant, minted for one
caller and expiring in minutes — see [File.SignedUrl](https://osysharp.com/reference/storage/file-signed-url/) for the same idea over a path. So it is not a value
an app can compute while a page is being drawn, and it is not something you would want to hold: it would go stale
under a reader who left the page open.

So the element carries the **row**, and the renderer asks for an address when it actually needs one, renews it before
it expires, and asks once however many places on the page show the same file. The access decision is your entity's
own `security {}` block and nothing else: a reader who may not read the asset is refused the address.

### What a reader sees when they may not see it   {#refused}
A refused image shows **nothing, and says so** in its alternative text, rather than the browser's broken-image icon.
That distinction is deliberate: "you may not see this" and "this app is broken" look identical on screen and are
different facts. Write an `alt` and the refusal is appended to it.

### Showing a photograph somebody uploaded   {#example}
The whole loop — a row that holds a picture, and a page that shows it:

```osy title="an entity with a photograph, and the page that shows it" test app=ui-image
using Osysharp.Storage;
using Osysharp.Ui;

entity Listing {
  [Required, MaxLength(140)] string Title;
  FileAsset? Photo;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

[Page("/listings")]
[AllowAnonymous]
component Listings() {
  live var rows = Listing.OrderBy(l => l.Title);
  render {
    Stack(gap: 4, p: 8) {
      foreach (var l in rows) {
        Row(gap: 3, align: Align.Center) {
          Image(fileAsset: l.Photo.Id, alt: l.Title, w: 120, rounded: Radius.Md);
          Text(l.Title);
        }
      }
    }
  }
}
```

`l.Photo` is the row and `l.Photo.Id` is what the element takes — the same spelling `PdfViewer` and `PdfThumbnail`
take, so every stored file is addressed the same way.

### A thumbnail rather than the full picture   {#thumbnail}
A list of twenty photographs should not send twenty full-size images to a phone. `Image.Thumbnail` makes a smaller
variant, stores it, and answers a `FileAsset` of its own — which is another thing to show:

```osy title="store a small variant once, then show that instead" test app=ui-image
using Osysharp.Images;

entity ListingThumb {
  [Required] Listing Of;
  FileAsset? Small;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

ListingThumb MakeThumb(Listing listing) {
  var thumb = new ListingThumb { Of = listing, Small = Image.Thumbnail(listing.Photo, 128) };
  return thumb;
}
```

Nothing is generated on upload: the variant exists the moment you ask for it, is deduplicated by content, and is
addressed by `Image(fileAsset: t.Small.Id)` exactly like the original.

### A picture in the app's own file store   {#path-store}
When the file is one the app put somewhere by path — a logo, a seeded asset, an [upload](https://osysharp.com/reference/ui/upload/) result — there is no
row and no signing. `File.Url` builds the address and `src` takes it:

```osy title="a path-keyed file: File.Url builds the address, src takes it" test app=ui-image
using Osysharp.Storage;

entity Brand {
  [Required, MaxLength(200)] string LogoPath;
  security { allow read when IsAuthenticated || IsAnonymous; }
}

string LogoUrl(Brand brand) { return File.Url(brand.LogoPath); }
```

## See also       {#see-also}
- [Files (addressing something the app stores)](https://osysharp.com/reference/storage/index/) — the two ways a file is stored, and which address each one has
- [Image.Thumbnail, Resize and Convert (a stored image, transformed)](https://osysharp.com/reference/storage/images/) — `Image.Thumbnail`, `Resize` and `Convert`: a stored image transformed into another one
- [File.Url](https://osysharp.com/reference/storage/file-url/) — the public address of a PATH-keyed file, which is what `src` wants
- [File.SignedUrl](https://osysharp.com/reference/storage/file-signed-url/) — a time-limited grant to one caller, over a path
- [upload](https://osysharp.com/reference/ui/upload/) — where a picture usually comes from
- [camera and microphone](https://osysharp.com/reference/ui/capture/) — a photograph from the camera, arriving as the same `UploadedFile`


---

<!-- https://osysharp.com/reference/ui/slots/ -->

# Slot (child content)

> A `Slot` marks where a component renders the content block its caller wrapped around it. Writing `Card { Text("hi"); }` passes `Text("hi")` as Card's children; Card renders them wherever it writes `Slot`. The passed content evaluates in the CALLER's scope, so a wrapper component (a card, a panel, a dialog) can frame arbitrary content without knowing what it is.

<!-- id: ui-slots · area: ui · stability: stable · html: https://osysharp.com/reference/ui/slots/ -->

## Summary        {#summary}
A **`Slot`** is the point where a component renders the **children its caller passed it** — the content projection
mechanism (React's `children`, Vue's `<slot>`). A component that writes `Slot` in its render tree is a **wrapper**:
its caller supplies a content block, and the wrapper decides *where* that content lands.

```osy title="a component with one slot" test app=ui-slots-basic
[Composable] component Card() {
  render { Box { Slot; } }        // whatever the caller wraps in a Card lands here, inside the box
}
```

Calling `Card { Text("hi"); }` renders `Text("hi")` **inside** Card's box. The card frames the content; it never
needs to know what the content is.

## Signature      {#signature}
```osy title="in the wrapper — default, named, and id-bearing slots" syntax
Slot;              // the default slot — renders the caller's children block here
Slot("header");    // a NAMED slot — renders the caller's `slot header { … }` fill here
Slot(id: field);   // …and NAME what lands here, so the wrapper's own caption can point at it
```

A caller passes the default content by writing a **content block** after a component call, and fills a **named**
slot with `slot <name> { … }`:

```osy title="at the call site — fill a named slot or the default" syntax
Card {             // the block below is Card's default children
  slot header {     // fills Card's Slot("header")
    Text("Title");
  }
  Text("body");     // untagged → Card's default Slot
}
```

## Description    {#description}
A component call takes an optional trailing `{ … }` **children block**. Those child nodes are handed to the called
component as its slot content. Wherever that component writes `Slot`, the caller's children render in place.

The projected content evaluates in the **caller's scope**, not the wrapper's. This is what makes wrappers reusable:
the content can read the caller's own state, props, and loop variables — the wrapper only positions it.

```osy title="slot content resolves in the CALLER's scope" test app=ui-slots-scope
[Composable] component Panel() {
  render { Box { Slot; } }
}

[Composable] component Greeting(string who) {
  render {
    Panel { Text(who); }         // `who` is Greeting's prop — resolved in Greeting's scope, not Panel's
  }
}
```

A wrapper that writes `Slot` but whose caller passes **no** children renders nothing at that position (never an
error). A component that never writes `Slot` simply ignores any children a caller passes.

### Named slots   {#named}
A wrapper with more than one insertion point gives each a **name**: `Slot("header")`, `Slot("footer")`. A caller
fills a named slot with a `slot <name> { … }` block; untagged children still fill the default `Slot`.

```osy title="named regions" test app=ui-slots-named
[Composable] component Card() {
  render {
    Box {
      Slot("header");    // the header region
      Slot;              // the default region
    }
  }
}

[Page("/")] [AllowAnonymous] [Render(CSR)]
component Home() {
  render {
    Card {
      slot header { Text("Title"); }   // → Card's Slot("header")
      Text("body");                    // → Card's default Slot
    }
  }
}
```

The slot name is **checked at compile time**: filling a slot the wrapper doesn't declare (a typo, or a slot that
doesn't exist) is a compile error, so a mis-named fill is caught before it ships. An editor completes the available
slot names from the wrapper you're calling. A named slot the caller doesn't fill renders nothing (it's optional).

Slots render identically whether a route is delivered server-side (pre-rendered HTML) or client-side, so a page
built from wrappers hydrates without a flash.

### Naming what lands in a slot   {#labelling}

A wrapper renders the caption and the caller renders the control, on two sides of a boundary. So a form component
looks perfectly labelled and is labelled for exactly one audience: people who can see the layout.

`Slot(id: <handle>)` closes that in the one direction that is well-defined — **the wrapper declares the handle and
claims the content it is filled with.** The caller writes nothing:

```osy title="a Field that names the control it is given" test app=ui-slots-naming
[Composable] component Field(string label) {
  render {
    Stack(gap: 1) {
      Text(label, labelFor: field);   // a real <label>, and it names…
      Slot(id: field);                // …whatever the caller puts here
    }
  }
}

[Page("/signup")] [AllowAnonymous] component SignUp() {
  string name = "";
  render { Field("Your name") { Input(value: name); } }
}
```

The caption is written **once**, at the call site, and the pair is a real `<label for>` — so clicking the words
focuses the field, which on a form of small controls is most of the hit area. Writing `label: "Your name"` on the
`Input` instead gives an accessible name and neither of those: the caption is then written twice, with nothing
keeping the two in step.

⚠ **The fill must be a single element.** `for=` names one element; with two roots there is no answer, and the first
one takes the name. Where a slot legitimately holds several things, name the control directly with `label:`.

## Examples       {#examples}
A reusable card wrapper framing page-specific content:

```osy title="card-wrapper" test app=ui-slots
component Card() {
  render { Box { Slot; } }
}

[Page("/welcome")]
[Render(SSR)]
component Welcome() {
  render {
    Card {
      Text("Welcome");
    }
  }
}
```

A wrapper with a named region plus its default content — the caller fills the named slot by name (a mis-typed name
would be a compile error) and leaves the rest for the default slot:

```osy title="named-slots" test app=ui-slots
component Panel() {
  render {
    Box {
      Slot("header");
      Slot;
    }
  }
}

[Page("/article")]
[Render(SSR)]
component Article() {
  render {
    Panel {
      slot header { Text("Title"); }
      Text("Body");
    }
  }
}
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the component archetype a slot lives in
- [layout primitives](https://osysharp.com/reference/ui/layout/) — the layout atoms (`Box`, `Stack`, `Row`) a wrapper frames its slot with
- [routes and pages](https://osysharp.com/reference/ui/routing/) — binding a wrapper-composed page to a route


---

<!-- https://osysharp.com/reference/ui/slot-template/ -->

# Slot(item) — let the caller decide what each row looks like

> `Slot(item)` renders the caller's template once for that item. It is how a component owns the list — the layout, the scrolling, the selection — while the app that uses it owns what a single row looks like.

<!-- id: ui-slot-template · area: ui · stability: preview · html: https://osysharp.com/reference/ui/slot-template/ -->

## Summary        {#summary}
A plain `Slot` places the caller's content once. `Slot(item)` renders it **once per item**, with a different value
each time — so one component can serve a list of anything.

## Signature      {#signature}
```osy syntax
// in the component — pass the item
Slot(<value>);

// at the call site — receive it
SomeComponent(items: xs) { x => … }
```

## Description    {#description}
Some components are about a *collection*: a picker, a list, a table. The component knows how to lay the collection
out; only the app knows what one row should say. `Slot(item)` is what connects those.

**The component passes the item:**

```osy title="inside the component — Slot hands each item out" syntax
[Composable] component Picker<T>(T[] options) {
  render {
    Stack {
      foreach (var o in options) { Slot(o); }
    }
  }
}
```

**The caller supplies a template**, naming the value it receives:

```osy title="at the call site — name the value the template receives" syntax
Picker(options: customers) { c =>
  Row { Text(c.Name); Hint(c.Region); }
}
```

That block is a **template**, not content: it runs once per `Slot(o)`, and `c` is a different customer each time.

**The template still sees everything around it.** Only the named value comes from the component; the rest of the
expression resolves where you wrote it, so a page's own state and the item can appear together:

```osy title="the template also sees the page around it" syntax
string search = "";

Picker(options: customers) { c =>
  Text(c.Name, bold: c.Name == search);     // `c` from Picker, `search` from the page
}
```

**A plain `Slot` is unchanged.** A component that just wraps its caller's content — a card, a panel, a dialog —
writes `Slot;` exactly as before. You only need the argument when the same content has to render more than once with
different values.

**A component may use both.** `Slot(current)` for the closed state of a dropdown and `Slot(o)` inside its list both
render the same template, with different values — which is usually what you want, since the selected row should look
like the rows it was chosen from.

### A repeated slot needs a template, and the compiler says so   {#repeated}

A fill is **placed, not copied**. The caller builds its content once and the component moves it to wherever the
`Slot` is — so a `Slot` reached once per row can only ever end up holding it in the **last** row, and every earlier
row renders empty. Both halves of that mismatch are compile errors rather than a blank page:

```osy title="refused — plain content for a slot inside a foreach" syntax
[Composable] component Rows<T>(T[] rows) {
  render { foreach (var r in rows) { Stack { Slot("cell"); } } }
}

Rows(rows: xs) { slot cell { Text("hi"); } }
// ✗ 'Rows' renders `Slot("cell")` inside a `foreach`, so this fill would be placed once per row …
```

**The fix is two-sided**, and so is the message: the caller writes a template, and the component gives that template
a datum to render with. A template is only ever invoked by `Slot(<name>, <datum>)`, so adding `r =>` alone swaps a
last-row-only page for an empty one — which is refused too, naming the component's half.

```osy title="the pair that works" syntax
[Composable] component Rows<T>(T[] rows) {
  render { foreach (var r in rows) { Stack { Slot("cell", r); } } }   // ← hand each invocation its row
}

Rows(rows: xs) { slot cell { r => Text(r.Name); } }                   // ← receive it
```

**Two mutually exclusive `if` arms are not repetition.** A component that writes the same `Slot` in a wide arm and a
narrow one renders one of them at a time, so plain content is correct there and is accepted.

**If a caller passes plain content to a component that expects a template**, nothing renders at that slot — the same
as any unfilled slot. Slots are optional by design.

## Examples       {#examples}

A picker that owns the list and lets its caller own the row:

```osy title="the control owns the LIST; the caller owns what a row looks like" test app=slot-template-picker
entity Customer { [MaxLength(100)] string Name; }

[Composable] component Picker<T>(T[] options) {
  render {
    Stack {
      foreach (var o in options) { Slot(o); }
    }
  }
}

component Home() {
  live var rows = Customer.ToList();
  render {
    Picker(options: rows) { c =>
      Text(c.Name);
    }
  }
}
```

A named per-row cell — the shape the refusals above steer to:

```osy title="one NAMED cell of a table — the shape the refusals above steer to" test app=slot-template-named-cell
entity Customer { [MaxLength(100)] string Name; }

[Composable] component Rows<T>(T[] rows) {
  render {
    Stack {
      foreach (var r in rows) { Row { Slot("cell", r); } }
    }
  }
}

component Home() {
  live var cs = Customer.ToList();
  render {
    Rows(rows: cs) { slot cell { c => Text(c.Name); } }
  }
}
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — declaring a component and its parameters
- [[Composable] — presentational components in public pages](https://osysharp.com/reference/ui/composable/) — marking a presentational component so any page can render it


---

<!-- https://osysharp.com/reference/ui/sort-by-column/ -->

# Sorting by a column the user picks

> A sortable table names its sort key with the chosen column's own selector, never with a string. Over rows already loaded the sort happens in memory; over a PAGED read the server does it, because ordering a page you already hold is a different question from ordering the set and then taking a page.

<!-- id: ui-sort-by-column · area: ui · stability: preview · html: https://osysharp.com/reference/ui/sort-by-column/ -->

## Summary        {#summary}
A column already knows how to read its value — that is what its selector is. So the chosen **column** names the
sort, and no string key ever enters the picture:

```osy title="click a header, sort by that column" test app=ui-sort-by-column
entity Report { [MaxLength(80)] string Title; decimal Total; }
class Column<T> { public string Label; public Func<T, string> Value; }

// The rows are PASSED IN, so they are already fetched and sorting them is a local matter.
[Composable] component Grid<T>(T[] rows, Column<T>[] columns) {
  Column<T> sortBy = columns[0];
  action SortBy(Column<T> c) { sortBy = c; }

  render {
    Stack(gap: 2) {
      Row(gap: 2) {
        foreach (var h in columns) {
          Pressable(onClick: () => SortBy(h)) { Text(h.Label, fontWeight: "600"); }
        }
      }
      foreach (var r in rows.OrderBy(sortBy.Value)) { Text(sortBy.Value(r)); }
    }
  }
}

[Page("/")] [AllowAnonymous]
component Home() {
  live var reports = Report.ToList();
  render {
    Grid(rows: reports, columns: [ new Column<Report> { Label = "Title", Value = r => r.Title },
                                   new Column<Report> { Label = "Total", Value = r => r.Total.ToString() } ]);
  }
}
```

A string key would rename silently and fail at run time. A selector is checked against the row type, so a column
that reads a field the row does not have is a compile error naming the row type.

## Signature      {#signature}

| form | where it runs |
|---|---|
| `rows.OrderBy(column.Value)` | in memory, over rows the client already holds |
| `rows.OrderByDescending(column.Value)` | in memory, descending |
| `Entity.OrderBy(column.Value).Take(n)` | on the SERVER, before the page is taken |

## Description    {#description}

### Loaded rows sort in memory   {#in-memory}
While the page **is** the table — every row is loaded — sorting the rows in hand sorts the table. `OrderBy` over a
list or a component's `T[]` parameter does exactly that, and nothing crosses the network.

Where the rows come from decides which sort you get, and it is not a detail. Rows **passed in** as a parameter have
already been fetched, so there is no read left to change and the sort is local. `OrderBy` written directly on a
**read** — an entity set, or a `live var` holding one — folds into that read instead, so the SERVER sorts. That is
what you want (sorting a page you were handed is the wrong question), and it is why the paged form below is the
same expression rather than a different one.

### A PAGED read sorts on the server   {#paged}
Once a read is paged, sorting the rows in hand is the wrong question: it re-orders the twenty rows you were given,
when what you asked for is the first twenty **of the ordered set**. Write the same expression on the entity read and
the server orders first:

```osy title="the server orders, then takes the page" test app=ui-sort-by-column-paged
entity Report { [MaxLength(80)] string Title; decimal Total; }
class Column<T> { public string Label; public Func<T, string> Value; }

[Page("/")] [AllowAnonymous]
component Home() {
  var columns = [ new Column<Report> { Label = "Title", Value = r => r.Title },
                  new Column<Report> { Label = "Total", Value = r => r.Total.ToString() } ];
  Column<Report> sortBy = columns[0];
  action SortBy(Column<Report> c) { sortBy = c; }

  live var page = Report.OrderBy(sortBy.Value).Take(20);

  render {
    Stack(gap: 2, p: 4) {
      Row(gap: 2) {
        foreach (var h in columns) {
          Pressable(onClick: () => SortBy(h)) { Text(h.Label, fontWeight: "600"); }
        }
      }
      foreach (var r in page) { Text(sortBy.Value(r)); }
    }
  }
}
```

Changing the column re-runs the read, so the page can gain and lose rows — which is the point, and the difference
you can see: the first twenty by name and the first twenty by total are not the same twenty.

### Two rules the compiler enforces on a paged sort   {#rules}
A paged sort is compiled ahead of time, so the columns it can sort by have to be knowable when it is compiled.

1. **The chosen column must be taken from the column list** — `sortBy = columns[0]`. That one line is what lets the
   compiler read every column the sort could use.
2. **The column list must be written out** where the component declares it. A list built by a function, or arriving
   as a parameter, has no contents to read, and a sort key guessed at would quietly return a different page.

Anything else is refused, naming what to change. Your own code never mentions how the choice reaches the server.

### Sorting state belongs beside the read   {#state}
For a paged sort the chosen column and the read live in the same component, because the read is what the sort
belongs to. A reusable grid takes the chosen column and a "sort by this" callback as parameters.

## Examples       {#examples}

```osy title="descending, and a computed sort key" test app=ui-sort-by-column-computed
entity Report { [MaxLength(80)] string Title; decimal Total; }
class Column<T> { public string Label; public Func<T, string> Value; }

// A selector is an ordinary expression, so a column can sort by something it COMPUTES rather than a stored field —
// which is the thing a string key could never have expressed.
[Composable] component Grid<T>(T[] rows, Column<T>[] columns) {
  Column<T> sortBy = columns[0];
  render {
    Stack(gap: 2) {
      foreach (var r in rows.OrderByDescending(sortBy.Value)) { Text(sortBy.Value(r)); }
    }
  }
}

[Page("/")] [AllowAnonymous]
component Home() {
  live var reports = Report.ToList();
  render {
    Grid(rows: reports, columns: [ new Column<Report> { Label = "Band", Value = r => r.Total > 150 ? "high" : "low" },
                                   new Column<Report> { Label = "Title", Value = r => r.Title } ]);
  }
}
```

## See also       {#see-also}
- [Func<T, R>](https://osysharp.com/reference/ui/function-value/) — what a column's `Value` is, and why a selector rather than a string
- [generic component](https://osysharp.com/reference/ui/generic-component/) — one `Column<T>` serving every row type
- [component](https://osysharp.com/reference/ui/component/) — state, actions and the render block


---

<!-- https://osysharp.com/reference/ui/shell-tabbed/ -->

# TabbedShell

> An app shell whose primary navigation is a horizontal strip of tabs under the brand row, and a bottom tab bar within thumb reach on a phone. It takes the same AppChrome and the same six slots as every other arrangement, so switching to it from another shell is one word.

<!-- id: ui-shell-tabbed · area: ui · stability: stable · html: https://osysharp.com/reference/ui/shell-tabbed/ -->

## Summary        {#summary}

`TabbedShell` puts the app's primary sections across the top on a pointer, and along the bottom edge on a phone.
Reach for it when the sections are **peers** — a handful of them, no deep hierarchy — and when a good phone
experience matters, because a bottom bar is one thumb-reach from every section where a drawer is a tap away from
even seeing them.

It reads exactly the same declaration as every other arrangement, so switching is one word:

```osy title="the same chrome, arranged as tabs" test app=ui-shell-tabbed
[Layout]
[AllowAnonymous]
component TabbedLayout() {
  action Palette() { }
  action Inbox() { }

  render {
    TabbedShell(new AppChrome {
      Product = "Expensely",
      Tagline = "Finance operations",
      Home = "/",
      User = new ShellUser { Name = "Olivia Rhye", Secondary = "Finance manager", Initials = "OR" },
      Nav = [
        new NavItem { Label = "Overview", To = "/", Icon = Icons.Home, Exact = true },
        new NavItem { Label = "Approvals", To = "/approvals", Icon = Icons.CheckCircle, Badge = "12", BadgeTone = Tone.Warning },
        new NavItem { Label = "Settings", To = "/settings", Icon = Icons.Gear, Children = [
          new NavItem { Label = "People", To = "/settings/people", Icon = Icons.Users },
        ] },
        new NavItem { Kind = NavKind.Section, Label = "Administration", Children = [
          new NavItem { Label = "Audit log", To = "/audit", Icon = Icons.File },
        ] },
      ],
    }) {
      Outlet(retain: 8);
      slot search { ShellSearch("Search requests, people or departments", onPress: Palette); }
      slot actions { ShellCountButton("Notifications", 3, onPress: Inbox) { Icon(Icons.Bell, size: 18); } }
    }
  }
}

[Page("/")] [Layout(TabbedLayout)] [Title("Overview")] [Render(CSR)] [AllowAnonymous]
component TOverview() { render { PageHead("Overview"); Card("This month") { Text("Nothing needs you."); } } }

[Page("/approvals")] [Layout(TabbedLayout)] [Title("Approvals")] [Render(CSR)] [AllowAnonymous]
component TApprovals() { render { PageHead("Approvals"); } }

[Page("/settings")] [Layout(TabbedLayout)] [Title("Settings")] [Render(CSR)] [AllowAnonymous]
component TSettings() { render { PageHead("Settings"); } }

[Page("/settings/people")] [Layout(TabbedLayout)] [Title("People")] [Render(CSR)] [AllowAnonymous]
component TPeople() { render { PageHead("People"); } }

[Page("/audit")] [Layout(TabbedLayout)] [Title("Audit log")] [Render(CSR)] [AllowAnonymous]
component TAudit() { render { PageHead("Audit log"); } }
```

## Signature      {#signature}

```osy syntax
TabbedShell(AppChrome chrome) {
  Outlet(retain: 8);          // the routed page
  slot search   { … }         // the brand row — `ShellSearch(…)` is shaped for it
  slot actions  { … }         // the brand row, right of search
  slot railFoot { … }         // no rail here: it sits under the page as a help card
  slot aside    { … }         // beside the page at 1100+, under it below that
  slot primary  { … }         // a task's commit bar — `ShellTaskBar`. See [FocusedShell](https://osysharp.com/reference/ui/shell-focused/)
}
```

Everything in `AppChrome`, `NavItem`, `ShellUser` and `MenuAction` is the shared contract — see [App shells](https://osysharp.com/reference/ui/shell/).

## Description    {#description}

### What sits where, at each width   {#bands}

| band | brand row | navigation |
|---|---|---|
| compact `< 768` | mark · page title · actions · identity | a **bottom tab bar**: up to five entries, plus "More" |
| pointer `768+` | brand · search · actions · identity | a horizontal **tab strip** on its own row, plus a **secondary strip** while you are inside a section that has sub-areas |
| wide `1100+` | the same | the same, and the page sits beside its `aside` |

**Two rows on a pointer, not one.** A single row has to divide one line between the brand, the tabs, the search
box, the page's actions and the identity chip — so the tabs, which are the point of the arrangement, get whatever
is left and begin overflowing at about eight entries on a 1280 screen. Two rows give the strip the full width. It
costs 52px of height on a surface that has plenty.

### What happens when the tabs stop fitting?   {#overflow}

**The strip scrolls.** It never wraps to a ragged second row, and there is no "More" menu on a pointer.

A measured priority-plus menu — count what fits, move the rest into an overflow — is the richer answer, and the
platform cannot honestly build it today. It needs a **per-element** width, and the only measurement a component can
read is `Layout.Width`, its own container's. Deriving the cut from label lengths instead was considered and
rejected for a specific reason: `Layout.Width` is null on the first render and arrives after it, so every page load
would paint a full strip and then jump tabs into an overflow that was not there a frame earlier — in the **common**
case, where everything fits, not the rare one. A layout that moves under the reader on every navigation is a worse
failure than a strip they have to scroll.

Scrolling costs nothing when the tabs fit, hides nothing permanently, and keyboard focus scrolls a tab into view
natively. The scrollbar itself is suppressed, because a horizontal bar under a tab row reads as a rendering fault.

### How does a phone reach the sections that did not fit?   {#more}

The bottom bar holds up to **five** entries — a count, not a measurement, and deliberately so: iOS and Android both
cap a tab bar at five by convention, so a deterministic cut *is* the platform behaviour rather than an
approximation of it. If the tail would be a single entry the bar simply shows all of them, because "four and a More
holding one" is worse than five.

Everything else lives in the **"More" sheet**, and the sheet holds the **whole nav tree** rather than only the tail.
That matters: a sheet holding only the overflow would leave a barred entry's *children* with nowhere to live, so a
phone reader could reach "Settings" and never "Settings → Billing". Showing everything is also what a "More" screen
is on both native platforms.

### What does a tab with children do?   {#groups}

**It navigates. That is all a tab ever does.** A tab is a *destination*, and one press does one thing.

A `NavItem` with `Children` goes to its own `To` when it has one, and otherwise to the first route beneath it
(`NavItem.FirstRoute`). An entry whose whole subtree holds no route at all is drawn as plain text rather than as a
link, because a link to nowhere accepts the press and does nothing.

Its children are not hidden behind that press. Once you are **inside** the section, they appear in a **secondary
strip** beneath the tab bar, carrying that section's areas and no others — the shape GitHub's repository tabs,
Stripe's dashboard and the Azure portal all converge on. Anything nested deeper than the strip's own level is
flattened into it rather than dropped.

⚑ **Why not a dropdown?** Because a tab that both navigated *and* opened a menu did two things on one press, which
is a **menu bar** (File / Edit / View) rather than a tab strip — a different interaction model, and one this shell
is not for. The strip also shows a section's areas *without* a press instead of hiding them behind one.

The strip appears only when the section you are in actually has sub-areas. A section with none costs nothing: no
empty rule, no reserved height, so the page does not shift as you move between the two kinds of section.

⚠ **The strip's landmark is the section's own name** — `"Settings sections"`, not a second `"Main navigation"`. Two
landmarks with the same name make a nav unnavigable by keyboard, and would make every `within:` in your tests
ambiguous.

The phone gets the same strip, under the slim top bar, for the same reason and from the same declaration. The
"More" sheet still holds the whole tree, so it remains the exhaustive index rather than the only way in.

⚠ **A top-level `Kind = NavKind.Section` becomes its children.** A bar has nowhere to put a heading and nothing to
do when one is pressed, so `Primary()` replaces the section with the entries under it. A rail has the room to draw
the heading; a strip does not. Both readings are right for their own arrangement.

### Reading the current tab   {#current}

Exactly one tab is ever lit: the shell asks `AppChrome.CurrentRoute` for the **longest** matching route in the whole
tree, so `/settings` and `/settings/people` do not both light up on the child's page. A route no tab covers — a
record detail, a wizard step — leaves no tab current, and the strip names the page on its trailing edge instead of
leaving it anonymous.

⚠ **Give a non-`/` home `Exact = true`.** The prefix rule claims every route under an entry and only `/` is exempt
from it, so an "Overview" at `/t` would otherwise light on every page beneath it.

## Examples       {#examples}

```osy title="a tab whose children become a secondary strip" syntax
new NavItem { Label = "Settings", To = "/settings", Icon = Icons.Gear, Children = [
  new NavItem { Label = "People", To = "/settings/people", Icon = Icons.Users },
] },
```

The primary strip is the `nav` landmark named "Main navigation", the same name every arrangement uses; the
secondary one is named after its section:

```osy title="driving the tabs and the section strip" syntax
Ui.Click("Settings");                              // ONE act — the tab navigates, and nothing else happens
Assert.OnPage("/settings");
Assert.Visible("People", within: "Settings sections");
Assert.Below("Settings sections", "Main navigation");   // under the bar, above the page
Ui.Click("Invoices");                              // a level deeper, flattened into the same strip
Assert.OnPage("/settings/billing/invoices");
```

## Notes          {#notes}

⚠ **The identity chip is in the brand row, not at the strip's end.** The strip needs its full width for tabs, which
is the reason the arrangement has two rows at all.

⚠ **`railFoot` has no rail here.** It is rendered under the page as a bounded help card rather than dropped — a slot
an app filled and a shell discarded is lost content, not lost layout.

⚠ **The secondary strip does not stick.** The tab bar pins because it is the app's orientation and true on every
page; the section strip belongs to the section's *content* and scrolls with it. Three pinned bars would eat a third
of a laptop's viewport before the page began.

⚑ **A shell holds no nav state on a pointer.** Which section you are in is read from the **route**, never from a
panel the reader had to open — so what the navigation shows and where you actually are cannot disagree.

## See also       {#see-also}

- [App shells](https://osysharp.com/reference/ui/shell/) — the shared `AppChrome` contract every arrangement reads
- [RailShell](https://osysharp.com/reference/ui/shell-rail/) — a permanent icon rail, when the canvas is the product
- [FocusedShell](https://osysharp.com/reference/ui/shell-focused/) — one task, no navigation at all. A focused route can live **inside** a tabbed app: `[Layout(…)]` is per-page, so a wizard is a route rather than an application
- [Navigation](https://osysharp.com/reference/ui/navigation/) — `Navigation.Routes`, `[Title]`, and what a shell reads from them
- [accessibility](https://osysharp.com/reference/ui/accessibility/) — landmarks, `role:`, `current:` and the naming props


---

<!-- https://osysharp.com/reference/ui/pdf-kit/ -->

# The PDF kit — a document viewer and a page thumbnail

> Two controls over a stored PDF: a reader with fit, zoom and a real text layer, and a thumbnail that draws one page as a picture. Shipped as an optional KIT you depend on with one line, not as a platform feature every app carries. This page says what you get, what it costs, which of the two you want, and the two lines that get them running.

<!-- id: ui-pdf-kit · area: ui · stability: stable · html: https://osysharp.com/reference/ui/pdf-kit/ -->

## Summary        {#summary}

`PdfViewer` puts a stored PDF on the screen: pages laid out to scroll or one at a time, a fit-to-width default, zoom
steps, and every page carrying transparent, selectable text over the rendering. You give it the **row** — a stored
file — and dress its toolbar with your own controls; it does the loading, the scaling and the painting.

`PdfThumbnail` is the other half: one page, at a width you choose, with no chrome and nothing to press — what a file
list wants beside a filename. The two share their renderer, so a page showing a list of thumbnails and then one open
document downloads it once.

It is a **kit**, so an app that never shows a document pays nothing for it. One line in the manifest and one `using`
turn it on.

```osy syntax
use Osysharp.Pdf@1;    // in app.osy — takes the dependency and pins the version
using Osysharp.Pdf;    // in the file that renders it
```

## Signature      {#signature}

```osy syntax
PdfViewer(
  fileAsset: doc.Pdf.Id,        // the stored file's row
  page: page,                   // which page is showing
  fit: "width",                 // width | page | actual
  zoom: 100,                    // a percentage of the fit scale
  layout: "continuous"          // continuous | single
)

PdfThumbnail(
  fileAsset: doc.Pdf.Id,        // the same row
  page: 1,                      // which page to picture
  width: 128                    // the drawn width in CSS pixels; the height follows the page
)
```

## Description    {#description}

### What you get, and what it costs    {#what-you-get}

| | Size | When it loads |
|---|---|---|
| The renderer | 425 KB + a 1.2 MB worker | the first document |
| Standard fonts | 762 KB | the first document that does not embed its own fonts |
| Scanned-image decoders | 1.5 MB | the first page holding a JBIG2 or JPEG 2000 image |

Nothing above is paid at mount, and nothing at all is paid by an app that does not depend on the kit.

The decoders are the entry worth reading twice. A **scanned** page — most of what an archive of correspondence holds
— is usually stored as JBIG2 or JPEG 2000, and the renderer degrades quietly when it cannot decode one: the page comes
out **blank** rather than failing. That is why they ship with the kit rather than being left as an option.

### Why it is a kit rather than a platform feature    {#why-a-kit}

Because most apps never show a document, and the ones that do should not make every other app carry a renderer.

There is a second reason, and it is the more interesting one. The browser will show a PDF on its own if you frame the
file — no kit, no bytes. What you get for free is *the browser's* viewer: chrome you cannot theme, behaviour that
differs between browsers, whatever accessibility that browser happens to give it, and nothing a test can read. A
canvas renderer draws the same everywhere, carries real text, and can be driven and asserted from your app's own
tests. For a document a citizen may need read aloud to them, that difference is the whole point.

### Where it lives, and how it is pinned    {#where-it-lives}

The kit travels with the platform: `use Osysharp.Pdf@1;` needs no network and no package install. Two pins guard it,
and they answer different questions:

| pin | asks | fails when |
|---|---|---|
| `minPlatform` | does this platform have what the kit's source names? | the kit composes over something you do not have |
| `contractVersion` | can this platform host the kit's control ABI? | your client's supported-contract set excludes it |

### The files, and what each is for    {#the-files}

| File | What it is |
|---|---|
| `pdf.osy` | The `control PdfViewer` declaration — props, events, commands, the toolbar slot, the chunks. This is what your app compiles. |
| `pdf-thumbnail.osy` | The `control PdfThumbnail` declaration, beside it. Two files rather than one because they are two controls, and a reader looking for either should find a file named after it. |
| `pdf.ts` | The viewer's shim. Yours to edit. |
| `pdf-thumbnail.ts` | The thumbnail's shim — a much shorter one: it draws a page and stops. |
| `pdf-open.ts` | What both shims need before they can draw: the renderer, and an open document. ONE copy of the signing round trip and the asset roots, because those are the parts that are security-shaped and two copies would drift on exactly them. |
| `scripts/build-chunks.mjs` | Builds the renderer, worker, font and decoder assets from your `node_modules`. |
| `package.json` | The build-time dependencies. None of them are runtime dependencies of your app. |
| `package-lock.json` | The EXACT versions those dependencies resolved to. Copy it with `package.json`: a kit that keeps its lock rebuilds the same bundle, and one that drops it picks up whatever the registry offers that day — a different bundle from the one that was tested, with nothing to say so. |
| `tsconfig.json` | Type-checking only — the bundler strips types without looking at them, so this is what makes the shim answer to its generated ABI. |
| `README.md` | How to build the kit and what each part is, for whoever picks it up next. |

### From an empty app to a working viewer — the sequence   {#sequence}

```osy title="the app declares the pinned dependency" syntax
app CaseFiles {
  use Osysharp.Pdf@1;       // the viewer
  use Osysharp.Storage;     // FileAsset, where the document lives
  model "model/**/*.osy";
}
```

```osy title="the page that uses it opens both namespaces" syntax
using Osysharp.Pdf;
using Osysharp.Storage;
```

### What the app passes in — a row, not a URL    {#app-side}

**Give it `fileAsset`, never a link.** A URL that lets a browser fetch a private file is a short-lived capability,
minted at the moment someone's authority is checked. Passed in as a prop it is minted when the *page* renders, which
is not when the document opens and not when the reader moves to a different one — so a viewer handed a link can be
holding one that has already lapsed. Handed the row instead, it asks for what it needs when it needs it, and the
answer is authorized by reading that file under the signed-in user's own authority. Your `FileAsset` security is the
whole access rule; the kit adds none of its own.

```osy title="a case file with its attachment on screen" test app=ui-pdf-kit
app CaseFiles {
  use Osysharp.Pdf@1;
  use Osysharp.Storage;
}

using Osysharp.Pdf;
using Osysharp.Storage;
using Osysharp.Ui;

entity CaseDocument {
  [MaxLength(200)] string Title;
  FileAsset Pdf;
}

[Page("/documents/{id}")] [AllowAnonymous]
component DocumentPage(Guid id) {
  live var doc = CaseDocument.Single(x => x.Id == id);
  int page = 1;
  int pages = 0;

  action OnPage(int n) { page = n; }
  action OnLoaded(int count) { pages = count; }

  render {
    Stack(gap: Space.SectionGap) {
      PageTitle(doc.Title);
      PdfViewer(fileAsset: doc.Pdf.Id, page: page, fit: "width",
                pageChanged: OnPage, loaded: OnLoaded) {
        // The viewer ships no toolbar. This is yours — your controls, your words, your order.
        slot Toolbar { c =>
          Row(gap: Space.Gutter, align: Align.Center) {
            Button("Previous", c.PreviousPage);
            Text($"Page {page} of {pages}");
            Button("Next", c.NextPage);
            Button("Zoom in", c.ZoomIn);
            Button("Zoom out", c.ZoomOut);
          }
        }
      }
    }
  }
}
```

Props worth knowing:

| Prop | What it does |
|---|---|
| `fit` | `width` fits the page across the space it was given — the reading default. `page` fits the whole page, which is unreadable in any panel narrower than a screen. `actual` is 1:1. |
| `zoom` | A percentage **of the fit scale**, so 100 means "as `fit` says" rather than 1:1. |
| `layout` | `continuous` scrolls the document; `single` shows one page, which is what a viewer beside a form wants, so a scroll gesture belongs to the page and not to the document. |

Events: `loaded(pageCount)` when the document opens, `pageChanged(page)` whenever the visible page changes — however
it changed, including by scrolling — and `failed(reason)` in words, because a document that will not open and a
document whose first page is genuinely white look identical.

Commands, for the toolbar you write: `NextPage`, `PreviousPage`, `ZoomIn`, `ZoomOut`, `ResetZoom`.

### The thumbnail, and when to reach for it instead    {#thumbnail}

`PdfThumbnail` draws one page and stops. No scroller, no paging, no zoom, no toolbar, no text layer — everything the
reader needs in order to be READ is exactly what a picture does not.

```osy title="a file row with its first page beside the name" syntax
Row(gap: Space.Gutter, align: Align.Center) {
  PdfThumbnail(fileAsset: file.Id, width: 30);
  Stack(gap: 0) { Strong(file.Name); FieldLabel(file.MimeType); }
}
```

| Prop | What it does |
|---|---|
| `fileAsset` | The row, as the viewer takes it. |
| `page` | Which page to picture. Almost always the first — but a cover sheet is a real thing, and so is picturing the page a search matched. |
| `width` | The drawn width in CSS pixels; the height follows the page's own aspect. A number rather than a size token because this is a raster: the value decides how many pixels are actually painted. |

It raises `loaded(pageCount)` — enough for a "3 pages" caption beside the picture — and `failed(reason)`.

**⚠ It downloads the document to make the picture.** There is no server-side rendering behind this: the bytes come
to the browser and page one is drawn there. That is right for a handful on screen and wrong for a list of five
hundred, for which the answer is a stored thumbnail — and the platform does not render one for a PDF yet. (It does
for an image: `Image.Thumbnail` produces a real stored file.)

**A failed thumbnail looks failed.** It keeps a page-shaped placeholder and carries the reason as its accessible
name, rather than collapsing to nothing — a 0×0 canvas and a document whose first page is blank are the same
picture, and a file list is exactly where that confusion costs someone an afternoon.

**Why two controls rather than one with a flag.** A `thumbnail: true` mode on the viewer would leave five of its
props, all five of its commands and two of its three events meaningless — a control where most of the surface
silently does nothing. The two share what is genuinely shared: one copy of the signing round trip, and the same
content-addressed renderer, so the second control costs a few kilobytes rather than a second copy of pdf.js.

### The theme tokens it reads    {#theme-tokens}

Both controls paint their surround from your theme, so they belong to your app rather than sitting in it as foreign
panels. They read `Colors.Bg`, `Colors.OnBg`, `Colors.TextMuted`, `Colors.Muted` (the thumbnail's placeholder when a
document will not open), `Shadow.Raised`, `Space.Gutter` and `Space.PagePad`. The page itself is the document's own
pixels and is not themed — a document is not yours to restyle.

### Forking it to change how it behaves   {#forking}

Declare a `control PdfViewer` of your own and it shadows the kit's, exactly as forking any kit control does. That is
the supported way to change the layout algorithm, add an annotation layer, or drop the decoders you do not need.

### Traps that cost real time    {#traps}

**A blank page is usually a missing decoder, not a broken document.** If a scan shows as an empty white page while a
text PDF renders, the file holds a JBIG2 or JPEG 2000 image and the decoders did not load — check that the kit's
asset chunks travelled with your build.

**`zoom` is relative to `fit`.** Setting `zoom: 100` does not mean 1:1; it means "whatever `fit` decided". For 1:1,
set `fit: "actual"`.

**Bind `page` to what the viewer reports, not to what you asked for.** Scrolling changes the visible page without
anyone pressing anything, so an app that only ever writes `page` and never listens to `pageChanged` will show a page
number that disagrees with the screen.

**Text you can see is not always text you can select.** A scanned document has no text layer of its own — the
renderer can only expose what the file contains. If selection and screen readers matter for scans, the documents need
to be run through text recognition before they are stored.

## Examples       {#examples}

```osy title="a compact viewer beside a decision form" syntax
Row(gap: Space.Gutter) {
  Box(width: "60%") {
    PdfViewer(fileAsset: application.Attachment.Id, layout: "single", fit: "page");
  }
  Box(width: "40%") { DecisionForm(caseFile: caseFile); }
}
```

```osy title="read-only, no toolbar at all — a preview in a list row" syntax
PdfViewer(fileAsset: attachment.Pdf.Id, fit: "page");
```

## See also       {#see-also}

- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — how a control is called, and what a slot is
- [commands — the verbs a control accepts](https://osysharp.com/reference/ui/control-commands/) — the verbs a control publishes, and how a toolbar drives them
- [chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/) — why the renderer is not paid for at mount
- [File.Url](https://osysharp.com/reference/storage/file-url/) — the capability the document lives in
- [Cell template (your own content in a control's cell)](https://osysharp.com/reference/ui/cell-template/) — putting a thumbnail (or anything else) inside a grid's cell
- [The markdown editor kit — a rich editor you opt into](https://osysharp.com/reference/ui/markdown-editor-kit/) — the other control-bearing kit
- [use](https://osysharp.com/reference/types/use/) — what `use` does, and why the version pin lives there


---

<!-- https://osysharp.com/reference/ui/index/ -->

# The UI (components, and the five layers under them)

> Every screen in an Osy# app is a `component` — the one archetype. A page is a component with a route on it, a layout is a component, a reusable widget is a component; `[Page]`, `[Composable]` and the rest are attributes on that one thing, not separate kinds. State is its fields, `live var` is the field that keeps up with the database, and `render { }` is the tree. This is the largest area in the language, so the page below it is a map: which of the five layers your question is in, and which of the sixty pages answers it.

<!-- id: ui-index · area: ui · stability: stable · html: https://osysharp.com/reference/ui/index/ -->

## Summary        {#summary}
**There is one archetype: the `component`.** A bounded reactive unit with typed props, reactive members, and a
declarative render tree. A page, a layout, a dialog, a reusable card — all the same declaration, told apart by
attributes on it:

```osy title="one archetype — a page, a composable and a plain component are the same declaration" syntax
component Card(string title) { … }                 // a reusable piece
[Page("/orders/{id}")] component Order(Guid id) { … }   // …the same thing, with a route
[Composable] component Badge(string text) { … }         // …composable into a public page
```

Inside one, the four kinds of member cover everything a screen does — and there is **no `state` keyword**:

| You want | You write |
|---|---|
| a value the component owns | a **field** — `int count = 0;` |
| a value that follows the database, or another value | **`live var`** — `live var rows = Order.Where(o => o.Open);` |
| something that happens when the user acts | **`action`** — `action Add() { count = count + 1; }` |
| a pure helper the tree may call | **`method`** — and `render` may call it ([Calling helpers from render](https://osysharp.com/reference/ui/render-calls/)) |
| what is on the screen | **`render { }`** — the tree |

And **five layers** sit under that, which is the other half of the model: almost every question about the UI is
really a question about which layer you are in.

| Layer | What it decides | You write |
|---|---|---|
| **Theme** | the app's design *values* — colour, spacing, radius, type | `theme T { Colors { … } }` |
| **Style** | what one box *looks like*, in a closed vocabulary of props | `variants { base { Bg = Surface; } }` |
| **Structure** | what is *on the screen* and how it is arranged | `render { Stack { Text("Hi"); } }` |
| **Behaviour** | what changes, and when | `live var`, `action`, `data` |
| **Route** | which component *is* a page, and who may see it | `[Page("/orders")] [Authorize(…)]` |

They stack in one direction: a **route** shows a **component**, whose **structure** is built from atoms, each styled
by **style props**, whose values come from **theme** tokens. Nothing skips a layer, and that is the whole design —
change a colour in the theme and every box that named it moves, with no recompile of anything else.

All five layers in one small app — read it top to bottom, and each section below tells you more about the layer you
just passed:

```osy title="every layer, once — theme, motion, style, structure, behaviour, route" test app=ui-index
// 1. THEME — the values, named once. Every token lowers to a CSS custom property.
theme Studio {
  Colors { Bg = "#F6F7F9"; OnBg = "#16181D"; Surface = "#FFFFFF"; Border = "#E3E6EA"; Accent = "#4F46E5"; }
  Radius { Card = "10px"; }
  FontSize { Body = "15px"; Section = "17px"; }
  FontWeight { Medium = "600"; }
}

// 1b. MOTION — looping, with no destination state, so it is an `animation` and not a `Transition`.
animation Pulse {
  Duration = "2s";
  Easing = EaseInOut;
  Repeat = Infinite;
  0%   { Opacity = 1; }
  50%  { Opacity = 0.5; }
  100% { Opacity = 1; }
}

// 2 + 3. STYLE + STRUCTURE — a `variants` recipe (static CSS) around a render tree built from atoms.
[AllowAnonymous]
component Card(string title) {
  variants {
    // A pseudo-state NESTS inside a variant value — a top-level block here would be a variant DIMENSION, which
    // has to match a parameter.
    base { Bg = Colors.Surface; Rounded = Radius.Card; P = 4; BorderW = 1; Border = Colors.Border; Hover { Border = Colors.Accent; } }
  }
  render {
    Stack(gap: 2) {
      Text(title, fontSize: FontSize.Section, fontWeight: FontWeight.Medium);
      Slot;
    }
  }
}

// 4 + 5. BEHAVIOUR + ROUTE — state an action moves, on a component the router can reach.
[Page("/")]
[AllowAnonymous]
component Home() {
  int count = 0;
  action Add() { count = count + 1; }
  meta { title = "Overview"; }
  render {
    Stack(gap: 4, p: 6, maxW: "640px", mx: "auto") {
      Card("Counter") {
        Row(gap: 3, align: Align.Center) {
          Text("Clicked " + count + " times", fontSize: FontSize.Body);
          Button("Add", onPress: Add);
          Box(w: "10px", h: "10px", rounded: Radius.Card, bg: Colors.Accent, animation: Pulse);
        }
      }
    }
  }
}
```

## Description    {#description}

### 1. Theme names the values, once    {#theme}
A **[theme tokens](https://osysharp.com/reference/ui/theming/)** block declares design tokens: `Colors`, `Space`, `Radius`, `FontSize`, `Shadow`, `Motion`,
`ZIndex`, `Length`, `Breakpoints`. Each token is a single value, and each lowers to one CSS custom property — which is
why re-theming needs no recompile of your components, and why a token can carry a **per-mode value**
(`Modes.Of(light: …, dark: …)`) that follows the OS dark-mode preference with no flash.

A token can be a whole colour **ramp** rather than one value — see **[color palettes](https://osysharp.com/reference/ui/palette/)**. Fonts the app ships are
**[web fonts — shipping a typeface with your app](https://osysharp.com/reference/ui/web-fonts/)**.

⚠ **A token is a scalar.** Anything with internal structure is not a token: that is why keyframes live in
**[animation — looping motion with no destination state](https://osysharp.com/reference/ui/animation/)** rather than in the theme, beside `entity` and `component` at the top level.

⚑ **Ask the compiler, do not guess:** `osy docs ui-theming` is the token vocabulary in full.

### 2. Style is one closed vocabulary    {#style}
**[style props](https://osysharp.com/reference/ui/styling/)** is the fixed list of **style props** — `Bg`, `P`, `Rounded`, `Position`, `Shrink`, `Cursor`, … —
each mapping to CSS. The vocabulary is **closed and compile-checked**: a misspelled `Backgroud = Surface` is an error,
not a line that silently styles nothing.

You apply them two ways, and they are the same vocabulary either way:
- **`variants { }`** on a component — the recipe. Compiles to static CSS classes, so it costs nothing at runtime and
  can carry pseudo-states (`Hover { }`) and responsive overrides (`Cozy { }`).
- **inline on an atom** — `Text("x", fontSize: FontSize.Body, color: Colors.Subtle)`, for a one-off that does not deserve a component.

Values are a **number** (a step on a scale — `P = 4` is `1rem`), a **keyword** from that prop's closed set
(`Display = Display.Flex`), a **theme token** by name, or a **literal string** for props that pass a raw CSS value through.

⚑ **Reach for a token, not a literal, whenever the value is part of the design.** `osy lint` flags a raw value written
three or more times (`ui-raw-style-literal-repeated`) and names the token to declare — a design decision living in N
places is exactly what the theme exists to prevent.

**[layout primitives](https://osysharp.com/reference/ui/layout/)** is deliberately separate: `gap`, `align` and `justify` are *arrangement*, not appearance, and take
their own path. If you are asking "how do these sit next to each other", that is the layout page, not this one.

⚑ **Do not derive the vocabulary by reading the renderer.** `osy docs ui-styling` prints every style prop in tables by
group; `osy docs ui-layout` prints `gap`/`align`/`justify`. A prop you invented because it seemed plausible is a
compile error at best and a silently ignored line at worst.

### 3. Structure is atoms, components and controls    {#structure}
Three kinds of thing render, and knowing which you want answers most "how do I build X" questions:

- **Atoms** — the **16** primitives the renderer itself owns: `Stack`, `Row`, `Box`, `Text`, `Button`, `Pressable`,
  `Input`, `TextArea`, `Link`, `Image`, `Icon`, `Svg`, `Path`, `Canvas`, `Markdown`, `Upload`. There is deliberately no
  `Card`, no `Modal`, no `Grid` atom. A grid is `Box(display: Display.Grid, cols: …)`; a card is a component you
  write. **The platform widens the style vocabulary rather than shipping components** — that is the standing rule,
  and it is why the atom list stays this short.
- **Kit controls** — **[Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/)**: the ready-made styled ones (`Field`, `Button`, `Table`, …), in scope for every
  app with no `using` and no `use`, forkable by declaring a component of the same name. **This is what you build a
  page out of**; a bare `Input` atom has no label, and `Field("Email", value: email)` is the labelled input with the
  spacing and the accessibility already in it. **[Pinning a kit version (using Ui@2)](https://osysharp.com/reference/ui/kit-versioning/)** pins a version.
- **Components** — **[component](https://osysharp.com/reference/ui/component/)**, what you write. Parameters are props; **[Slot (child content)](https://osysharp.com/reference/ui/slots/)** takes children;
  **[[Composable] — presentational components in public pages](https://osysharp.com/reference/ui/composable/)** governs reuse from a public page; **[generic component](https://osysharp.com/reference/ui/generic-component/)** covers `Dropdown<T>`;
  **[Calling helpers from render](https://osysharp.com/reference/ui/render-calls/)** is the call syntax; **[Visitor](https://osysharp.com/reference/ui/visitor/)** handles a heterogeneous tree.
- **Controls** — **[control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/)**: a foreign widget (chart, data grid, map) implemented in JavaScript and described
  to the compiler by a `control` block, so its call sites type-check like anything else. Its styling knobs are
  **[styles — a control's own look knobs](https://osysharp.com/reference/ui/control-styles/)**, its imperative verbs **[commands — the verbs a control accepts](https://osysharp.com/reference/ui/control-commands/)**, its lazy assets
  **[chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/)**, what it reports about itself **[probe — what a control says about itself](https://osysharp.com/reference/ui/control-probe/)**.

App-shipped **[icons](https://osysharp.com/reference/ui/icons/)**, **[SVG assets](https://osysharp.com/reference/ui/svg-assets/)**, **[textures](https://osysharp.com/reference/ui/textures/)** and **[sound](https://osysharp.com/reference/ui/sound/)** are referenced by name
and checked at compile time.

⚑ **`osy kit` lists every bundled control with its signature and a worked example** (`osy kit <Control>` prints its
whole source, which is also how you fork it). `osy kit --atoms` lists the 16 primitives, each marked container or
not — i.e. whether `gap`/`align`/`justify` apply to it at all.

### 4. Behaviour is what changes, and when    {#behaviour}
**[The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/)** is the core: `var` is a snapshot, **`live var` subscribes** — a distinction worth knowing before
you write anything, because a list that never refreshes is almost always a missing `live`. **[on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/)** covers
mount/unmount, **[Pending](https://osysharp.com/reference/ui/pending/)** the in-flight state, **[skeleton](https://osysharp.com/reference/ui/skeleton/)** what stands in before the first result
lands, and **[A failing query](https://osysharp.com/reference/ui/query-failure/)** what happens when a read fails.

**Writing data is [creating & saving data](https://osysharp.com/reference/ui/data-mutation/)**, and it is the one place UI differs from a server function: an edit applies
**instantly and stays visible while the user keeps working**, and **`UnitOfWork.Commit()`** is what sends the
accumulated edits to the server atomically. A form commits once, on Save; a page that saves per action (ticking a
to-do *is* the save) commits in each verb. Both are correct — what is never correct is a page with no Save and no
`UnitOfWork.Commit()`, where the write is discarded with no error. A read written **inside** a body is
**[reading data inside an action](https://osysharp.com/reference/ui/read-in-a-body/)**; **[Dialog.Open / Dialog.Ask / Dialog.Confirm / Dialog.Discard](https://osysharp.com/reference/ui/dialogs/)** is where the unit of work is a choice you make (`Inherit` vs `Root`).

Input handling: **[on change](https://osysharp.com/reference/ui/on-change/)**, **[onEnter](https://osysharp.com/reference/ui/on-enter/)**, **[onEscape](https://osysharp.com/reference/ui/on-escape/)**, **[debounce](https://osysharp.com/reference/ui/debounce/)**,
**[Validation](https://osysharp.com/reference/ui/validation/)**, **[Clipboard](https://osysharp.com/reference/ui/clipboard/)**, **[pointer](https://osysharp.com/reference/ui/pointer/)**, **[keys](https://osysharp.com/reference/ui/keys/)**, **[drag](https://osysharp.com/reference/ui/drag/)**.
**[Connection](https://osysharp.com/reference/ui/connection/)** is the app's own surface for "the server dropped".

### 5. A route makes it a page, and decides who may see it    {#route}
**[routes and pages](https://osysharp.com/reference/ui/routing/)** makes a component a page and gives it a URL: `[Page("/catalog/{slug}")]` captures the segment as a
parameter, `[Layout(AppShell)]` wraps it, and `[Render(CSR)]` / `[Render(SSR)]` chooses whether the first response
already carries the content. **[Navigation](https://osysharp.com/reference/ui/navigation/)** moves between pages.

**[page authorization (policies)](https://osysharp.com/reference/ui/authorize/)** is the access gate — and the default is the important part: a routed component **requires auth
unless it says `[AllowAnonymous]`**. Never the other way round. **[canPress / canEdit / canSee](https://osysharp.com/reference/ui/policy-controls/)** reflects a policy into the
UI (a button that disables itself because the rule says so, rather than because someone remembered to check).
**[Session.CurrentUser](https://osysharp.com/reference/ui/current-user/)** is who is being shown the page; **[Visitor](https://osysharp.com/reference/ui/visitor/)** is the opaque browser id for someone who
has not signed in — a name, never a credential.

### Proving a screen works    {#testing}
A UI test is an ordinary `[Test]`: **[Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/)** — `Ui.Visit` opens a route, `Ui.Click` presses what a person
would press, `Ui.Fill` types, and the same `Assert.*` verbs ask the questions, with your real security rules on.
`within:` is how you address one row when several read alike. `osy docs testing-ui` is the page; `osy docs testing`
is the model underneath it.

### Where things are NOT    {#not-here}
The questions that most often send people to the wrong page:

- **"How do I space these out?"** → `gap`/`align`/`justify` are **[layout primitives](https://osysharp.com/reference/ui/layout/)**, not style props.
- **"How do I make this move?"** → an A→B state change is `Transition` (a **[theme tokens](https://osysharp.com/reference/ui/theming/)** motion token, applied as a
  style prop). Looping motion with no end state is **[animation — looping motion with no destination state](https://osysharp.com/reference/ui/animation/)**.
- **"Why is my list stale?"** → **[The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/)**. It is `var` where you wanted `live var`, far more often than
  it is anything else.
- **"Where do I put the save?"** → **[creating & saving data](https://osysharp.com/reference/ui/data-mutation/)**. There is no `Save()` on a row; there is
  `UnitOfWork.Commit()` on the unit of work.
- **"Why is my third-party script blocked?"** → **[What an app page is allowed to load](https://osysharp.com/reference/ui/content-security-policy/)**. A control must ship what it needs
  rather than fetch it at run time.

### Read these four first    {#reading-order}
If you are starting cold, four pages get you productive and the rest are reference:

1. **[component](https://osysharp.com/reference/ui/component/)** — how to declare one and render it.
2. **[layout primitives](https://osysharp.com/reference/ui/layout/)** — how boxes sit next to each other.
3. **[theme tokens](https://osysharp.com/reference/ui/theming/)** then **[style props](https://osysharp.com/reference/ui/styling/)** — in that order: tokens first, then the props that name them.
4. **[The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/)** — the `var` / `live var` distinction.

Then **[routes and pages](https://osysharp.com/reference/ui/routing/)** + **[page authorization (policies)](https://osysharp.com/reference/ui/authorize/)** when you want a real page, **[creating & saving data](https://osysharp.com/reference/ui/data-mutation/)** the moment it has
to save, and **[control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/)** when you need something the atoms cannot express. **[Writing a component — what differs from C#](https://osysharp.com/reference/ui/csharp-differences/)** is
worth a skim if you are coming from C#.

## The pages      {#the-pages}
The whole area, grouped by the question that sends you to it.

**The unit itself**
- [component](https://osysharp.com/reference/ui/component/) — the one archetype; props, members, render
- [Writing a component — what differs from C#](https://osysharp.com/reference/ui/csharp-differences/) — what a component body does *not* do the way C# does
- [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) — `var` vs `live var`, and how a change re-renders only what read it
- [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) — `on mount` / `on unmount`
- [generic component](https://osysharp.com/reference/ui/generic-component/) — a component with type parameters
- [[Composable] — presentational components in public pages](https://osysharp.com/reference/ui/composable/) — `[Composable]`, for a presentational piece a public page composes

**Building the tree**
- [layout primitives](https://osysharp.com/reference/ui/layout/) — `Stack` / `Row` / `Box`, and `gap` / `align` / `justify`
- [Slot (child content)](https://osysharp.com/reference/ui/slots/) · [Slot(item) — let the caller decide what each row looks like](https://osysharp.com/reference/ui/slot-template/) · [Cell template (your own content in a control's cell)](https://osysharp.com/reference/ui/cell-template/) — taking children, per-item templates, a control's cells
- [Naming a value in render](https://osysharp.com/reference/ui/render-binding/) — naming a value inside `render`
- [Calling helpers from render](https://osysharp.com/reference/ui/render-calls/) — calling a pure helper from a render expression
- [Visitor](https://osysharp.com/reference/ui/visitor/) — rendering a heterogeneous tree
- [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) — rendering stored markdown as content
- [Canvas](https://osysharp.com/reference/ui/canvas/) — a drawing surface and the `Draw.*` verbs
- [Canvas 3D](https://osysharp.com/reference/ui/canvas-3d/) — a lit, shadowed 3D scene on the same canvas: cameras, lights, fog and meshes
- [App shells](https://osysharp.com/reference/ui/shell/) — the app shell: one `AppChrome` declaration, four arrangements of it

**Look**
- [style props](https://osysharp.com/reference/ui/styling/) — the style-prop vocabulary
- [theme tokens](https://osysharp.com/reference/ui/theming/) — tokens; [color palettes](https://osysharp.com/reference/ui/palette/) — colour ramps
- [animation — looping motion with no destination state](https://osysharp.com/reference/ui/animation/) — looping motion
- [web fonts — shipping a typeface with your app](https://osysharp.com/reference/ui/web-fonts/) · [icons](https://osysharp.com/reference/ui/icons/) · [SVG assets](https://osysharp.com/reference/ui/svg-assets/) · [textures](https://osysharp.com/reference/ui/textures/) · [sound](https://osysharp.com/reference/ui/sound/) — the assets an app ships
- [accessibility](https://osysharp.com/reference/ui/accessibility/) — `role:`, `label:`, and the state props

**Ready-made**
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — the bundled controls; [Pinning a kit version (using Ui@2)](https://osysharp.com/reference/ui/kit-versioning/) — pinning one
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — declaring a foreign widget
- [styles — a control's own look knobs](https://osysharp.com/reference/ui/control-styles/) · [commands — the verbs a control accepts](https://osysharp.com/reference/ui/control-commands/) · [chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/) · [probe — what a control says about itself](https://osysharp.com/reference/ui/control-probe/) — a control's four blocks
- [The markdown editor kit — a rich editor you opt into](https://osysharp.com/reference/ui/markdown-editor-kit/) — the optional rich markdown editor
- [The chart kit — line, column, bar, scatter and candle, with no JavaScript](https://osysharp.com/reference/ui/chart-kit/) — the optional charts, with no JavaScript at all
- [The barcode kit — a QR and barcode scanner you opt into](https://osysharp.com/reference/ui/barcode-kit/) — the optional QR and barcode scanner

**Data**
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — creating, editing, deleting, and `UnitOfWork.Commit()`
- [reading data inside an action](https://osysharp.com/reference/ui/read-in-a-body/) — a read written inside an action or hook
- [Pending](https://osysharp.com/reference/ui/pending/) · [skeleton](https://osysharp.com/reference/ui/skeleton/) · [A failing query](https://osysharp.com/reference/ui/query-failure/) — in-flight, stand-in, and failed
- [Sorting by a column the user picks](https://osysharp.com/reference/ui/sort-by-column/) — sorting by a column the user picks
- [Validation](https://osysharp.com/reference/ui/validation/) — the entity's rules, shown beside the field

**Input and events**
- [on change](https://osysharp.com/reference/ui/on-change/) · [onEnter](https://osysharp.com/reference/ui/on-enter/) · [onEscape](https://osysharp.com/reference/ui/on-escape/) · [debounce](https://osysharp.com/reference/ui/debounce/) — the everyday handlers
- [pointer](https://osysharp.com/reference/ui/pointer/) · [keys](https://osysharp.com/reference/ui/keys/) · [drag](https://osysharp.com/reference/ui/drag/) — pointer, held keys, drag-to-a-number
- [on every](https://osysharp.com/reference/ui/cadence/) — `on every`; [on settled — run something once, when a stream finishes](https://osysharp.com/reference/ui/on-settled/) — once, when a stream finishes
- [Clipboard](https://osysharp.com/reference/ui/clipboard/) — writing to the system clipboard
- [sound](https://osysharp.com/reference/ui/sound/) — playing the audio an app ships
- [camera and microphone](https://osysharp.com/reference/ui/capture/) — the camera and the microphone: photograph, record, and what a refusal looks like
- [upload](https://osysharp.com/reference/ui/upload/) — the file picker, and the `UploadedFile` both it and the camera answer
- [Func<T, R>](https://osysharp.com/reference/ui/function-value/) — `Func<T, R>`, so a component can be told *how* to get a value

**Route, access, session**
- [routes and pages](https://osysharp.com/reference/ui/routing/) — `[Page]`, params, `[Layout]`, `[Render(CSR)]`
- [Navigation](https://osysharp.com/reference/ui/navigation/) — moving between pages
- [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) — the secure-by-default gate; [canPress / canEdit / canSee](https://osysharp.com/reference/ui/policy-controls/) — `canPress` / `canEdit` / `canSee`
- [Session.CurrentUser](https://osysharp.com/reference/ui/current-user/) — `Session.CurrentUser`; [Visitor](https://osysharp.com/reference/ui/visitor/) — the pre-sign-in browser id
- [Dialog.Open / Dialog.Ask / Dialog.Confirm / Dialog.Discard](https://osysharp.com/reference/ui/dialogs/) — an overlay, and which unit of work it edits in
- [Connection](https://osysharp.com/reference/ui/connection/) — the app's connection-loss surface
- [What an app page is allowed to load](https://osysharp.com/reference/ui/content-security-policy/) — what a page is allowed to load

**Measuring the real box**
- [Layout.ScrollHeight and Layout.ScrollWidth](https://osysharp.com/reference/ui/scroll-extent/) — `Layout.ScrollHeight` vs `Layout.Height`
- [Layout.TextWidth](https://osysharp.com/reference/ui/text-measurement/) — how wide a string will actually paint

## See also   {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the unit everything else hangs off.
- [style props](https://osysharp.com/reference/ui/styling/) — the full style-prop vocabulary, group by group.
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the token system the style props draw from.
- [layout primitives](https://osysharp.com/reference/ui/layout/) — arrangement, which is deliberately not styling.
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — how a screen writes, and where the commit is.
- [Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/) — driving the screen from a `[Test]`, with the security rules on.


---

<!-- https://osysharp.com/reference/ui/barcode-kit/ -->

# The barcode kit — a QR and barcode scanner you opt into

> A live camera scanner for QR codes and barcodes, shipped as an optional KIT you depend on with one line. It owns the camera and the decode loop and raises `scanned(text, format)`; your app supplies a box to draw in and an action to run. It is also the platform's proof that a control may ship a WebAssembly module.

<!-- id: ui-barcode-kit · area: ui · stability: preview · html: https://osysharp.com/reference/ui/barcode-kit/ -->

## Summary        {#summary}

The platform ships **mechanism**, never components. `Osysharp.Barcode` is a complete barcode and QR scanner built on
that mechanism, distributed as an ordinary kit — you depend on it in one line and nothing is copied into your
project:

```osy syntax
app Warehouse {
  use Osysharp.Barcode@1;
  model "model/**/*.osy";
}
```

It opens the camera, watches the picture, and raises `scanned(text, format)` when it reads a code. Everything about
*reading a symbol* — the camera, the frame loop, the decoder — is inside the control.

⚠️ **This is not the same thing as [camera and microphone](https://osysharp.com/reference/ui/capture/).** `Camera.Start()` / `Camera.Capture()` are for taking a
photograph or a recording and keeping the file. This is for reading what a picture *says* and keeping the text. An
app can use both; they do not interfere.

## Signature      {#signature}

```osy syntax
BarcodeScanner(scanned: Found)                       // everything, back camera, four looks a second
BarcodeScanner(scanned: Found, formats: qr)          // QR only — materially faster on a phone
BarcodeScanner(scanned: Found, active: armed)        // false releases the camera
```

`scanned` is an event, so it binds to an action taking `(string text, string format)`. The control has no default
size: put it in a box that has one.

## Description    {#description}

### The whole thing, at a call site   {#app-side}

```osy title="a page that reads a code and shows it" test app=ui-barcode-kit
// ⚠ This example DECLARES the control inline, because a documentation example compiles on its own with no manifest
// to carry a `use`. In your app you write `use Osysharp.Barcode@1;` instead and delete this block — the declaration
// arrives with the kit. What follows is `scan.osy`, trimmed to what this page uses.
control BarcodeScanner {
  contractVersion "1.1"
  participation headless
  props {
    bool active = true;
    [Values(any, qr, linear)] string formats = "any";
    [Values(back, front)] string facing = "back";
    int intervalMs = 250;
    int repeatAfterMs = 2000;
  }
  events {
    scanned(string text, string format);
    failed(string reason);
  }
  chunks { Core; Wasm; }
  probe { bool scanning; int scans; string? last; string? failure; }
}

[Page("/scan")] [AllowAnonymous]
component ScanPage() {
  string code = "";
  string problem = "";
  bool armed = true;

  action Found(string text, string format) { code = text; armed = false; }
  action Broke(string reason) { problem = reason; }
  action Again() { code = ""; problem = ""; armed = true; }

  render {
    Stack(gap: 3, p: 4) {
      Stack(h: "16rem") {
        BarcodeScanner(scanned: Found, failed: Broke, active: armed, formats: qr);
      }
      if (problem != "") { Text(problem); }
      if (code != "") {
        Text(code);
        Button("Scan another", onPress: Again);
      }
    }
  }
}
```

Note `active: armed`. A scanner that cannot be switched off is a camera that is always on — turning it off after a
read is both the polite thing and the thing that puts the browser's recording indicator out.

### `repeatAfterMs` is the prop that makes it usable   {#repeats}

A label held in front of the camera is read on **every** pass — four times a second at the default interval. So the
same text is reported once and then suppressed for `repeatAfterMs` (2 seconds by default). Without that, one label
held for ten seconds produces forty events, and an app that appends a row per scan collects forty rows.

Set it to `0` if you genuinely want every read — a counter, or a diagnostic.

### Narrowing `formats` is a real speed-up   {#formats}

| value | what it looks for |
|---|---|
| `any` | every format the decoder knows |
| `qr` | QR and Micro QR |
| `linear` | the retail and logistics 1-D family — EAN, UPC, Code 39/93/128, ITF, Codabar, DataBar |

The decoder spends time proportional to how many families it must try, so `qr` on a phone is noticeably faster than
`any`. `format` on the `scanned` event names the exact symbology it found (`QRCode`, `EAN-13`, …), not the family
you asked for.

### When it cannot run, it says so   {#failure}

`failed(reason)` carries words meant for a person: the camera was refused, there is no camera, another app holds it,
the decoder would not load. It is raised **once per cause**, not once per frame — so an app can show it as a message
without four of them arriving a second.

Like [camera and microphone](https://osysharp.com/reference/ui/capture/), this needs a **secure page**: browsers give no camera to plain `http://` beyond localhost.

### What it costs, and when   {#cost}

| | Size | When it loads |
|---|---|---|
| the control | 3 KB | on mount |
| the decoder glue | 37 KB | the first time a scanner is **activated** |
| the decoder itself | 1.07 MB | the same moment, as WebAssembly |

A page that mounts a scanner with `active: false` and never arms it downloads neither chunk. An app that never
writes the `use` carries nothing at all.

### It is also the proof that a control may ship WebAssembly   {#wasm}

The decoder is ZXing's C++ library compiled to WebAssembly, and it reaches the browser through nothing special: a
`.wasm` is an ordinary control asset, pinned by content address, served same-origin, and reached through the ordinary
[chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/) mechanism. If you are shipping a control of your own around a native library, this kit is the
worked example — see its `README.md` for the one function everything turns on.

## See also       {#see-also}

- [camera and microphone](https://osysharp.com/reference/ui/capture/) — the camera and microphone as verbs, for taking a photograph or a recording
- [chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/) — how an asset a control loads on demand is declared and served
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — declaring a foreign control of your own
- [The markdown editor kit — a rich editor you opt into](https://osysharp.com/reference/ui/markdown-editor-kit/) — the other bundled control kit, and the first one
- [upload](https://osysharp.com/reference/ui/upload/) — a file a person chooses, which is the other way bytes arrive from a browser


---

<!-- https://osysharp.com/reference/ui/chart-kit/ -->

# The chart kit — line, column, bar, scatter and candle, with no JavaScript

> Line, area, column, bar, scatter and OHLC candle marks over one shared value scale, plus a Pie — an optional KIT you depend on with one line. Marks are written as CHILDREN, so the call site reads as the picture being described. It ships no bundle at all: a charting library is the surface people are most certain needs JavaScript, and this one does not.

<!-- id: ui-chart-kit · area: ui · stability: stable · html: https://osysharp.com/reference/ui/chart-kit/ -->

## Summary        {#summary}

**Charts are a KIT, not a platform feature** — you depend on it in one line and nothing is copied into your project.
It draws line, area, column, horizontal bar, scatter and OHLC candle marks over **one shared value scale**, with a
`Pie`/donut sibling beside it.

```osy title="one line to depend on it, one `using` to write it" syntax
app Dashboard {
  use Osysharp.Charts;
  model "model/**/*.osy";
}
```

⭐ **It ships NO BUNDLE.** `Osysharp.Markdown` carries a JavaScript shim because it wraps a third-party editor nobody
should rewrite; this kit is `.osy` files and nothing else. A charting library is the surface people are most certain
needs JavaScript — hit-testing a hover against pixel coordinates on a `<canvas>` is most of one — and the hover here
is an ancestor-conditioned style variant that lowers to a descendant CSS rule. No pointer handler, no coordinates,
no measurement.

## Signature      {#signature}

```osy title="the shape: a chart, its categories, and its marks as children" syntax
Chart(title: string, subtitle: string, labels: string[], height: int, chrome: ChartChrome) {
  Column(label: string, values: double[]);   // …or Line, Area, Bar, Dots, Candles
  Axis(side: AxisSide, ticks: int, title: string, min: double, max: double);
}
```

**The marks are CHILDREN, and that is the point.** They are config records, so the call site reads as the picture
being described rather than as an array being assembled. Giving the chart `labels` is what asks for a category axis
— you rarely declare `Axis(side: Bottom)` yourself.

A bare enum member is the spelling (`side: Left`), exactly as in any other component call; `AxisSide.Left` also
works and is what you would write in C#.

## Description    {#description}

### What it draws   {#marks}

| | |
|---|---|
| **marks** | line, area, column, horizontal bar (GROUPED for more than one series, or `stacked: true`), scatter, OHLC candles |
| **axes** | a value axis with round-number ticks, a category axis, optional gridlines, a title, an explicit min/max |
| **hover** | a crosshair and a readout naming **every** series' value in the hovered category |
| **legend** | automatic for two or more series; `position:` on any edge or `None`, `interactive: true` to toggle a series off |
| **palette** | eight validated hues in fixed order, never cycled |
| **annotations** | a reference `Rule`, a shaded `Band`, a labelled `Note` — **none of them a series** |
| **table** | `Table()` below the chart, or `Table(position: Instead)` in place of it — a real `role: Grid` |
| **bare mode** | `chrome: Bare` — the sparkline: no card, axes, gridlines, legend, heading or hover |

### The whole vocabulary, in one place   {#controls}
Fifteen controls. `Chart` and `Pie` are the containers; everything else goes inside one of them.

| control | it is | what it takes |
|---|---|---|
| `Chart(title, subtitle, labels, …)` | the container for every category chart | the category `labels`, then marks as children |
| `Pie(title, subtitle, slices, …)` | the container for a part-of-whole chart | `Slice` children, or a `slices:` array |
| `Line(label, values)` | a mark — a series as a line | one `double[]`, one per category |
| `Area(label, values)` | a mark — a filled line | as `Line`; `stacked: true` on the chart to stack them |
| `Column(label, values)` | a mark — vertical bars | as `Line` |
| `Bar(label, values)` | a mark — horizontal bars | as `Line` |
| `Scatter(label, values)` | a mark — points | as `Line` |
| `Candle(label, values, opens, highs, …)` | a mark — OHLC candles | four arrays, one per category |
| `Slice(label, value)` | a mark — one wedge of a `Pie` | a single number |
| `Axis(side, grid, ticks, …)` | the value or category axis | `AxisSide`, and whether to draw gridlines |
| `Legend(position, interactive)` | the series key | `LegendPos`, and whether clicking toggles a series |
| `Rule(value, label)` | an ANNOTATION — a reference line | the value to sit at |
| `Band(from, to, label)` | an ANNOTATION — a shaded range | the two bounds |
| `Note(category, value, label)` | an ANNOTATION — a label at one point | where to put it |
| `Table(position)` | the same data as a real `role: Grid` | below the chart, or `Instead` of it |

⚠ **An annotation is not a series** — `Rule`, `Band` and `Note` carry no data, take no palette colour and never
appear in the legend. See [[#annotations]], which is the half people get wrong.

`osy kit <name>` prints any of them in full, and `osy kit --for "<what you want>"` finds one by what it does.

### One crosshair rule for every mark kind   {#hover}

An invisible band of full-height cells sits over the plot, one per category. Hovering a cell reveals a crosshair and
a readout of **every** series at that category — the shared-crosshair behaviour a line chart wants, which happens to
be right for columns and dots too. **What you learn on a bar chart holds on a line chart.**

The readout stays inside the plot: a readout centred on the first or last category would hang past the edge and be
clipped, losing exactly the label you hovered to read. It measures with `Layout.TextWidth` and shifts **only** the
cells that would overflow, so a middle category is still centred on its crosshair — a flip, not a re-anchor.

⚑ **Measured from the TEXT, not from the box.** Asking `Layout.Width` for the readout's own width from inside the
readout answered 109.0 where the painted box was 114.6: a self-measurement lags its own content by construction,
because the number describes one layout pass and the content may be from another. `Layout.TextWidth` is a pure
function of the string and the font, so there is nothing to lag.

### An annotation is not a series   {#annotations}

Things that are true about the chart but are **not in the data**: a target, an acceptable range, the day something
happened.

```osy title="a band, a rule and a note — none of them a series" syntax
Chart(title: "Revenue against target", labels: months) {
  Column(label: "Revenue", values: revenue);
  Band(from: 55000.0, to: 65000.0, label: "Acceptable");
  Rule(value: 60000.0, label: "Target");
  Note(category: "May", value: 73000.0, label: "v2 launch");
  Axis(side: Left);
}
```

⛔ **An annotation gets no legend entry, no palette slot, cannot be toggled off, and DOES NOT MOVE THE SCALE** —
because none of those things is true of "the target is 60k". Modelling a target as a flat one-value `Line` is the
usual shortcut and it costs exactly those four: the legend grows an entry nobody clicks, the palette shifts under
the real series, and a 500k target stretches the axis until every real bar is a stub in the bottom eighth.

An out-of-range annotation is **clamped and says so both ways** — `↑` on the face of the chart, and "(above the top
of this chart)" in the accessible name. A clamped line that looked exactly like a met target would be its own kind
of lie.

⚠ **A `Note` names its category by NAME.** An index would be a lie waiting to happen: insert a month at the front
and every note silently shifts one slot. A name that is not in `labels` is said out loud.

### Why one value scale   {#one-scale}

Two y-axes let any two series be made to cross wherever you like. One shared scale is the honest picture, and it is
why a target belongs in an annotation rather than in a second axis.

## Examples       {#examples}

The commonest dashboard chart there is — magnitude by category, with a target over it:

```osy title="the commonest dashboard chart there is — magnitude by category, with a target over it" test app=ui-chart-kit
app ChartExample {
  use Osysharp.Charts;
  model "model/**/*.osy";
}

using Osysharp.Charts;

[Page("/")]
[Render(CSR)]
[AllowAnonymous]
[Title("Revenue")]
component Dashboard() {
  string[] months  = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
  double[] revenue = [42000.0, 55000.0, 48000.0, 61000.0, 73000.0, 69000.0];
  double[] target  = [40000.0, 50000.0, 55000.0, 60000.0, 65000.0, 70000.0];

  render {
    Chart(title: "Revenue against target", subtitle: "First half", labels: months, height: 280) {
      Column(label: "Revenue", values: revenue);
      Line(label: "Target", values: target);
      Rule(value: 60000.0, label: "Target");
      Axis(side: AxisSide.Left, ticks: 4);
    }
  }
}
```

`demo/chart-demo` is the worked gallery: every mark kind at a realistic size (`/`), a live query feeding a chart
(`/live`), the palette's eight slots and what happens at nine series (`/palette`), and candles over sessions
(`/time`).

## See also       {#see-also}

- [The markdown editor kit — a rich editor you opt into](https://osysharp.com/reference/ui/markdown-editor-kit/) — the other shipped kit, and the one that DOES carry a bundle
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — the 46 bundled controls, which need no `use` at all
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the tokens a chart's surface, ink and grid read


---

<!-- https://osysharp.com/reference/ui/markdown-editor-kit/ -->

# The markdown editor kit — a rich editor you opt into

> A full rich-text markdown editor — sections, partial saves, a block menu, tables, find and replace, maths and diagrams — shipped as an optional KIT you depend on with one line, not as a platform feature every app carries. This page says what you get, what it costs, why it is a kit, and the two lines that get it running.

<!-- id: ui-markdown-editor-kit · area: ui · stability: stable · html: https://osysharp.com/reference/ui/markdown-editor-kit/ -->

## Summary        {#summary}

The platform ships **mechanism**, never components: a way to declare a foreign control, a way to load its assets on
demand, a way for it to read your theme. It ships no editor *by default*. The **markdown editor kit** is a complete
one built on that mechanism, distributed as an ordinary kit — you depend on it in one line and nothing is copied
into your project:

```osy syntax
app Notes {
  use Osysharp.Markdown@1;    // the editor
  model "model/**/*.osy";
}
```

That is the architecture, not a limitation. The client runtime has **zero runtime dependencies**, deliberately. An
editor is 1.1 MB of somebody else's library choices; a diagram engine is 3.4 MB more. Those belong to the apps that
asked for them — pinned by content hash in your lock file, resolved offline, and costing nothing at all to an app
that never writes the `use`.

⚠️ **Most pages do not want this.** If you only need to *display* markdown, use [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) — it is in the
platform, has no dependencies, needs no `use`, and renders identically on the server and the client. It renders
neither maths nor diagrams. Reach for the kit when a person is going to **write** into the document.

## Signature      {#signature}

With the dependency declared, the editor is an ordinary control at a call site:

```osy syntax
MarkdownEditor(
  ownerType: "Article",     // the entity that owns the document
  ownerId: article.Id,      // which row
  property: "Body"          // the Markdown property on it
)
```

## Description    {#description}

### What you get, and what it costs    {#what-you-get}

| | Size | When it loads |
|---|---|---|
| The editor | ~1.1 MB | on mount |
| Maths (`$…$`, `$$…$$`) | 207 KB + 21 KB | the first document containing a formula |
| Diagrams (```` ```mermaid ````) | 3.4 MB, split across ~100 files | the first document containing a diagram — and then only the parts that diagram needs |

The two optional halves are [chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/), so those numbers are what a document that *uses* them costs.
Measured on the reference app: a document of prose makes **one** asset request; adding a formula makes two more;
adding a state diagram fetches an entry plus the handful of engine parts that one diagram type reaches — well under a
quarter of the 3.4 MB on disk.

What the editor does: a rendered view and a source view, section identity that survives arbitrary edits, partial saves
with per-section preconditions, conflict recovery, a block handle and slash menu, an outline rail, tables with
resizable columns, code highlighting, a selection toolbar, a link popover, find and replace, footnotes, private
images, maths and diagrams.

### Why it is a kit rather than a platform feature    {#why-a-kit}

Three reasons, in the order they will matter to you.

**The client has no runtime dependencies.** Everything the platform's own runtime does, it does with its own code.
Building an editor into it would add ProseMirror, CodeMirror and their trees to every app on the platform, including
the ones that render a dashboard. As a kit, the cost lands only where the `use` is written.

**It is built on the public contract, and that is checked.** The shim imports its own siblings, its generated ABI
typings and third-party libraries — nothing from platform internals — and the build proves it: a generated
conformance file makes the type-checker hold `mount` to the declaration. So the kit is evidence that the
foreign-control contract is sufficient, rather than a claim about it. Yours can do everything it does.

**Different apps want different editors.** A knowledge base and a comment box want different affordances; the kit is
one good answer, not the only one. It is `app-forkable` like every kit — take it, change it, and your copy wins.

### Where it lives, and how it is pinned    {#where-it-lives}

The kit is published at **`github:osysharp/markdown-kit`**, and a copy ships with the platform, so
`use Osysharp.Markdown@1;` resolves **offline** — no network, no `node_modules`, nothing to install.

`osyrin.lock` records what you resolved to, and a control kit pins **two independent compatibility facts**:

| pin | asks | fails when |
|---|---|---|
| `minPlatform` | does this platform have the atoms the kit's source names? | the kit composes over a renderer primitive you do not have |
| `contractVersion` | can this platform host the kit's control ABI? | your client's supported-contract set excludes it |

They are separate because a kit can satisfy one and fail the other — this kit targets contract `1.1` and names no
new atom at all. Each is checked at resolve, with its own message, so a failure tells you which of the two you have.

### The files, and what each is for    {#the-files}

You need none of this to USE the kit — it is what the kit is made of, for anyone reading it or forking it.

| File | What it is |
|---|---|
| `markdown.osy` | The `control` declaration — props, events, commands, chrome slots, chunks. This is what your app compiles. |
| `markdown.ts` | The shim: the editor itself. Yours to edit. |
| `markdown-theme.css` | Its stylesheet, in terms of your theme's tokens. |
| `math.ts` | The maths document model — how `$…$` parses and serializes. |
| `diagrams.ts` | The diagram node view — how a ```` ```mermaid ```` fence is drawn. |
| `section-map.ts` | Section identity: which edit belongs to which stored section. |
| `styles.d.ts` | One line that tells the type-checker a `.css` import arrives as a string. Easy to leave behind, and without it the shim does not type-check. |
| `scripts/build-chunks.mjs` | Builds the maths and diagram chunk assets from your `node_modules`. |
| `package.json` | The build-time dependencies. None of them are runtime dependencies of your app. |
| `package-lock.json` | The EXACT versions those dependencies resolved to. Copy it with `package.json`: a kit that keeps its lock rebuilds the same bundle, and one that drops it picks up whatever the registry offers that day — which is a different bundle from the one that was tested, with nothing to say so. |
| `tsconfig.json` | Type-checking only — the bundler strips types without looking at them, so this is what makes the shim answer to its generated ABI. |
| `README.md` | How to build the kit and what each part is, for whoever picks it up next. |

### From an empty app to a working editor — the sequence   {#sequence}

From an empty app to a working editor, in full:

```osy title="the app declares the pinned dependency" syntax
// app.osy
app Notes {
  use Osysharp.Markdown@1;
  model "model/**/*.osy";
}
```

```osy title="the page that uses it opens the namespace" syntax
// the page that uses it
using Osysharp.Markdown;
```

```bash
osy compile      # resolves the pin, ships the kit's bundle and chunks, compiles the app
```

Nothing is copied into your project and there is no JavaScript toolchain in the loop: the declaration arrives with
the `use`, and the bundle rides the compile from the platform's own kit cache. `osy lock` writes the pin;
`osyrin update markdown` moves it forward within the major you declared.

### Forking the kit to change how it behaves   {#forking}

The kit is `app-forkable`, which is the whole point of the tier. Take the shim, put it in your own project, register
it with `osy control add`, and your `control MarkdownEditor` shadows the kit's by name everywhere — including inside
anything that renders it. From then on it is ordinary app code: `osy control build` re-bundles and re-pins it after
every edit to the shim, and nothing in the platform has an opinion about what you changed.

### What the app passes in — a document address, not text   {#app-side}

The editor takes the **address** of a document, not its text. A document is a section-structured thing the editor
rewrites continuously; passing its contents as a prop would mean re-marshalling the whole document on every keystroke.

```osy title="an article page with the editor on its body" test app=ui-markdown-editor-kit
// ⚠ This example DECLARES the control inline, because a documentation example compiles on its own with no manifest
// to carry a `use`. In your app you write `use Osysharp.Markdown@1;` instead and delete this block — the declaration
// arrives with the kit. What follows is `markdown.osy`, trimmed to what this page uses.
control MarkdownEditor {
  contractVersion "1.1"
  participation headless
  props {
    string ownerType;
    Guid ownerId;
    string property;
    bool recordScoped = false;
    [Values(comfortable, compact)] string density = "comfortable";
    bool outline = false;
  }
  chunks { Math; MathCss; Diagrams; }
}

entity Article {
  [MaxLength(200)] string Title;
  Markdown Body;
}

[Page("/articles/{id}")] [AllowAnonymous]
component ArticlePage(Guid id) {
  var article = Article.Where(a => a.Id == id).First();
  render {
    Stack {
      Text(article.Title);
      MarkdownEditor(ownerType: "Article", ownerId: article.Id, property: "Body", outline: true);
    }
  }
}
```

Props worth knowing:

| Prop | What it does |
|---|---|
| `readOnly` | Renders the document without editing affordances. |
| `recordScoped` | Edits join the **page's** unit of work and land when the record commits. Default `false`: the document keeps its own lifetime, which is what a standalone document — and any agent writing to it — wants. |
| `face`, `density` | The reading typeface and how much air the document gets. |
| `outline` | A heading outline down the side. Worth the width on a long document only, which is something the page knows and the control does not. |
| `findQuery`, `replaceWith` | The find engine's state. The editor ships **no** find bar — it holds the engine and raises `findRequested`, and the bar is yours to design. |

A document's `---` front-matter binds to ordinary queryable members — see [[FrontMatter] — a document's header as typed data](https://osysharp.com/reference/entity/front-matter/).

The editor also declares a `Toolbar` **chrome slot** ([commands — the verbs a control accepts](https://osysharp.com/reference/ui/control-commands/)): your own content, rendered inside the
editor's toolbar, handed the editor's commands so your buttons can drive it.

### The theme tokens it reads    {#theme-tokens}

The editor inherits your app's look. It reads your base tokens — `Colors.Surface`, `Colors.OnSurface`,
`Colors.Border`, `Colors.Muted`, `Colors.Primary`, and the `Fonts` family — and a nested `Markdown` group for the
document's own typography, so a document can differ from the rest of the app without either being hard-coded:

```osy syntax
theme {
  Colors { Markdown { Link; Quote; CodeBg; } }
  Fonts  { Markdown { Body; Heading; Mono; } }
}
```

⚠️ **A theme leaf name is global.** A nested group namespaces the CSS variable, not the name — `Fonts { Markdown {
Heading } }` and the `Fonts { Heading }` most apps already have are the same leaf. Name them so they do not collide.

Diagrams are themed from these same tokens, with one wrinkle worth knowing: a diagram engine **bakes** its colours
into the picture when it draws it, so switching to dark mode re-renders rather than re-styles. The kit watches for
that and redraws.

### Traps that cost real time    {#traps}

**The bundle is a build artifact.** This matters only if you FORKED the kit: editing `markdown.ts` changes nothing
until you re-bundle, and the hash changes when you do, so it must be re-registered — `osy control build` does both.
Using the kit as a dependency, there is nothing to build.

**Rename a component, restart the dev server with `--reset`.** A stale instance keeps the old component bound to its
route, and `compile` reports success.

**A single-file chunk must be self-contained; a package need not be.** This is the distinction that decides how you
register something — see [chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/).

**Register your own build output, not `node_modules`.** A published package's distribution directory carries every
build flavour and its type definitions; the diagram library's is 83 MB across 1167 files. The kit's build script does
this correctly — copy its approach if you add a chunk of your own.

## Examples       {#examples}

An article editor with the app's own find bar in the editor's toolbar:

```osy title="an article editor putting the app's own find bar in the toolbar" syntax
[Page("/articles/{id}")]
component ArticlePage(Guid id) {
  var article = Article.Where(a => a.Id == id).First();
  var find = "";

  render {
    MarkdownEditor(
      ownerType: "Article",
      ownerId: article.Id,
      property: "Body",
      outline: true,
      findQuery: find
    ) {
      slot Toolbar { c =>
        Row {
          TextInput(value: find, placeholder: "Find");
          Button("Next", onPress: c.FindNext);
          Button("Previous", onPress: c.FindPrev);
        }
      }
    }
  }
}
```

A comment box wants the opposite settings — compact, no outline, and scoped to the record so the comment and its body
commit together:

```osy title="a comment box wants the opposite — compact and record-scoped" syntax
MarkdownEditor(
  ownerType: "Comment",
  ownerId: comment.Id,
  property: "Text",
  density: compact,
  recordScoped: true
)
```

## See also       {#see-also}

- [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) — display markdown with no dependencies, when nobody is editing
- [chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/) — how the optional maths and diagram halves are loaded
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — declaring a foreign control of your own
- [commands — the verbs a control accepts](https://osysharp.com/reference/ui/control-commands/) — the chrome-slot mechanism the toolbar uses
- [[FrontMatter] — a document's header as typed data](https://osysharp.com/reference/entity/front-matter/) — binding a document's `---` header to entity members


---

<!-- https://osysharp.com/reference/ui/reactivity/ -->

# The reactivity & lifecycle model

> How an Osy# component comes alive and stays in sync: declarations are live value bindings, `on mount`/`on unmount` are once-only lifecycle bodies, and `on change` is a tracked reaction. A change re-renders ONLY the slots that read it — never the whole page — and a region that leaves disposes its own live queries and reactions automatically.

<!-- id: ui-reactivity · area: ui · stability: stable · html: https://osysharp.com/reference/ui/reactivity/ -->

## Summary        {#summary}
An Osy# component is **reactive by construction**. You *declare* what its values are; the runtime keeps them current and
re-renders only what actually changed. There is no re-render call, no dependency array, no manual subscribe/unsubscribe.
This guide is the mental model behind [component](https://osysharp.com/reference/ui/component/)'s members — read it once and the rest of the UI reference falls
into place.

Four kinds of member make up the model:

| Member | Runs | For |
|---|---|---|
| `live var x = …` / a server read | **continuously** — a live value binding, recomputed whenever anything it reads changes | *what a value IS* |
| `var x = …` | **once**, at setup — then it holds whatever it was last assigned | *state you assign to* |
| `on mount { … }` | once, before first paint | one-time imperative **setup** |
| `on change { … }` | at mount, then on every tracked change | a **reaction** — push a value outside the component |
| `on unmount { … }` | once, at teardown (after children) | one-time **teardown** |

**"Anything it reads" includes anything a helper it calls reads — WITH ONE EXCEPTION, AND IT IS THE CLOCK.** A
`live var` that calls a method or a top-level function tracks the DATA that function read. It does **not** track a
clock read inside it, and the compiler refuses that shape rather than letting it go stale:

| Written | Recomputes when |
|---|---|
| `live var due = items.Where(i => i.At < DateTime.UtcNow).ToList();` | the clock ticks, or `items` changes |
| `live var due = items.Where(i => IsDue(i, DateTime.UtcNow)).ToList();` — the instant passed IN | the same |
| `live var due = items.Where(IsDue).ToList();` — `bool IsDue(Item i) => i.At < DateTime.UtcNow;` | ⛔ **REFUSED** |

> ⛔ **THIS TABLE SAID "the same" FOR THE THIRD ROW UNTIL 2026-09-01, AND THE COMPILER HAS NEVER AGREED.** Written
> exactly as that row showed, `osy validate` answers:
> *"`live due` reads the clock through `IsDue(…)`, and a clock read inside a function does NOT advance — it is
> evaluated once… Reactivity is decided where the read is WRITTEN, not where the function is called."*
> ⚑ **And that refusal's own `for more` pointer names THIS PAGE**, so a reader who followed it arrived at the
> sentence that had just been rejected. Measured on eval run 23 of `012`, which wrote the documented form, was
> refused, and spent four calls re-reading this page trying to reconcile the two.
> **Read the clock where the value lives, or pass the instant in.**

## Description    {#description}

### The lifecycle of an instance   {#lifecycle}
An instance runs this sequence, once:

1. **Setup — declarations initialize.** `var`/`live var` fields take their initial value; server reads *kick off* (their
   rows stream in). A `live var` is a value BINDING and stays current for the instance's whole life; a plain `var`
   is a value you HOLD — its initialiser runs once and it changes only when something assigns to it.

   ⚠ That distinction is the one readers get wrong. A derived value written as a plain `var` looks right, renders
   right on the first paint, and then never moves — so reach for `live var` whenever the value is *computed from*
   something that can change, and plain `var` only for state you assign to yourself.
2. **`on mount` — once, before the first paint.** One-time imperative setup: seed an editable `new Entity{}` ghost, run
   a sequence, prime some state. Because it runs before first render, what it sets is already on screen in the first
   paint. (Data *loading* is a declaration — a query — never `on mount`.)
3. **First render — the DOM is built once**, and each dynamic slot (a text expression, a prop, an `if` condition, a
   `foreach` source) gets its own tiny reactive binding to exactly the values it reads.
4. **Steady state — a change wakes only what read it.** When a value changes, only the slots and `on change` blocks that
   *read that value* re-run — **not the whole page**. A one-row edit patches one text node; an unrelated field elsewhere
   is untouched.
5. **`on unmount` — once, at teardown.** Runs when the instance goes away, *after* its children have torn down. The
   instance's own live queries and reactions are disposed automatically at the same moment.

### Declarations vs. `on mount` — the one distinction to internalize   {#declaration-vs-mount}
A **declaration** says what data *is* — a pure value binding that stays live:

```osy title="a declaration — recomputes when its sources change" syntax
live var open = orders.Where(o => o.Status == Status.Open);   // recomputes when orders (or the filter) change
```

`on mount` is one-time **imperative** work — a `new Entity{}` you intend to *edit* (a declaration's `new T{}` is a plain
object, not a committable ghost), a load-time fetch, a startup side effect:

```osy title="on mount — imperative setup that must happen once" syntax
on mount { draft = new Organization {}; }   // a committable ghost in the page overlay, seeded once
```

Reach for a declaration first; reach for `on mount` only when the work is imperative and must happen exactly once.

### What a `live var` may be — and why an ordinary server function isn't one   {#live-var}
Which one it is follows from its initializer:

| Initializer | What you get | Stays current because |
|---|---|---|
| an **entity read** — `Invoice.Where(…)`, `from Invoice where …` | a **reactive query** | it subscribes to data changes *and* refetches when its dependencies change |
| a **projected read** — `Folder.Select(f => new FolderNode { … })` | a **reactive query of values** | same subscription, but each row is a plain projected shape (see below) |
| an expression over **client values** — `draft?.Name ?? "…"`, `a + b` | a **tracked computed** | it recomputes synchronously whenever a value it read changes |
| a **`stream<T>` call** — `Tail(path)`, `Ask(question)` ([yield — a function that produces results over time](https://osysharp.com/reference/function/yield/)) | a **live append-only list** | the stream *is* the subscription — the server holds the connection open and pushes, so there is nothing to poll and nothing to invalidate |

A call to an ordinary **server** function is none of these, so it is a compile error:

```osy title="✗ a live var cannot call a server function" syntax
live var files = FilesInFolder(selectedId);   // ✗ FilesInFolder runs on the server
```

Nothing subscribes such a value to data changes, and a tracked computed cannot recompute synchronously — it would have
to hand off to the server mid-render. Both ways out are spelled out by the diagnostic:

```osy title="the two ways out — fetch once, or inline the query" syntax
var files;                                            // ✓ fetch once, imperatively
on mount { files = FilesInFolder(selectedId); }

live var files = FileAsset.Where(f => f.FolderId == selectedId);   // ✓ inline the query — genuinely reactive
```

And a third, when the answer *builds up* rather than changing — make the function a `stream<T>` ([yield — a function that produces results over time](https://osysharp.com/reference/function/yield/)).
That removes the objection rather than working around it: a stream is its own subscription, so each result renders the
moment it arrives.

```osy title="a third way — make the producer a stream" syntax
stream<FileInfo> FilesInFolder(Guid id) { … yield return file; … }   // the producer
live var files = FilesInFolder(selectedId);                          // ✓ items appear as they are found
```

Inlining the query is almost always what you actually wanted: it re-runs when `selectedId` changes *and* when the
underlying rows change, which is the behaviour the server-function spelling only appeared to offer.

A **client-side** function is fine — it is a tracked computed like any other expression, so `live var greeting =
Shout(name)` compiles and recomputes when `name` changes. The rule keys on where the callee *runs*, not on the fact
that it is a call.

#### A projected `live var` — a live list of a shape you declared   {#projected}
A reactive query may **project into a `class`** ([Select (projections)](https://osysharp.com/reference/query/select/)), exactly as a function return may — so a `live var`
can hold a live list of the shape you actually want to render, not the raw entity:

```osy syntax
live var nodes = Folder.Select(f => new FolderNode {   // a live list of FolderNode — reshaped, and reactive
  FolderId = f.Id,
  Name     = f.Name,
  ParentId = f.Parent?.Id ?? Guid.Empty
});
```

It subscribes to the **source** entity (`Folder`) just like an entity read, so it refreshes on commit — create or delete
a folder and the list updates itself, with nothing to reload. What differs is the rows: a projection has **no row
identity**, so each row is a plain **value** of your shape rather than a tracked entity. You read its fields (`node.Name`)
and pass it on; there is simply no per-row entity to edit or track through it — the whole result refreshes together when
the source data changes. Reach for it wherever you would return a projected `class` from a server read, but want the
result to stay live instead of being fetched once.

### Fine-grained updates — why "on change" is tracked, not "every render"   {#tracking}
`on change` is **dependency-tracked**: it subscribes to exactly the reactive values its body reads, and re-runs only when
one of those changes. That is the whole reason the name is `on change` and not "effect that runs every render" — a block
that re-ran on every render would be waste, and the name forbids that reading. Any reactive read counts, not only a
`live var`: a plain state field reassigned by an action wakes it too. See [on change](https://osysharp.com/reference/ui/on-change/).

### Can I hold editable state as class values?   {#class-values}
A [class](https://osysharp.com/reference/class/index/) is an in-memory shape with no row behind it, so a component field holding a `List<T>` of
them is an obvious way to carry working state — a set of selections, a draft split, a basket of lines. The question
that follows is *if I write to one of those objects, does the screen move?*, and **the answer has two halves that
point in opposite directions.** Reading one is fully reactive. Binding a control to one is refused. Design against
one half alone and you will either write a reassignment you did not need, or an edit that cannot compile.

**Reading a class field is tracked like any other read.** A render slot that reads `p.Weight` subscribes to that
field, and a write through the object — from an action, through the list, through any alias of it, since a class is
a reference type ([[class-index#reference]]) — wakes exactly the slots that read it and nothing else. It is the same
[fine-grained tracking](#tracking) as everything else on this page; a class value is not a blind spot in it.

⚠ **So you do NOT need `picks = picks.ToList();` after mutating an element.** That reassignment is a reflex carried
in from frameworks that diff by list identity, and here it buys nothing: the write already re-rendered, and
rebuilding the list only makes the runtime redo work it had done. Write in place.

```osy title="an in-place write to a class field re-renders — the list is never reassigned" test app=ui-reactivity
class Pick {
  public string Name = "";
  public int Weight = 1;
  public Pick(string name) { Name = name; }
}

[Page("/picks")]
[AllowAnonymous]
[Render(CSR)]
component Picks() {
  List<Pick> picks = [ new Pick("Ann"), new Pick("Bo") ];

  int Total() { int t = 0; foreach (var p in picks) { t = t + p.Weight; } return t; }
  action Bump(Pick p) { p.Weight = p.Weight + 1; }   // in place — nothing reassigns `picks`

  render {
    Stack(gap: 3) {
      foreach (var p in picks) {
        Row(gap: 2) { Text(p.Name); Text("w=" + p.Weight); Button("Bump " + p.Name, onPress: () => Bump(p)); }
      }
      Text("Total shares: " + Total());
    }
  }
}

[Test]
void mutating_a_class_field_in_a_list_rerenders() {
  Ui.Visit("/picks");
  Assert.Visible("Total shares: 2");
  Ui.Click("Bump Ann");
  Assert.Visible("Total shares: 3");   // the derived total moved, from one field written in place
}
```

**But a control's two-way `value:` cannot target a class field.** A two-way binding has to write *back* somewhere,
and a class value has no row behind it to write through — so rather than hand you a field that accepts typing and
saves nothing, the compiler refuses it:

```osy title="✗ what a class field cannot be — a two-way binding target" syntax
foreach (var p in picks) { NumberField("Weight", value: p.Weight); }   // ✗ refused, at compile time
```

```text title="what the compiler says when you try it anyway"
ERROR  RESOLVE_ERROR  UI: cannot two-way bind to `p.Weight` — `p` is a `class`, and a class value has no row behind
it to write through, so the edit would be read-only. A two-way target is an assignable field of this component, or a
field of an ENTITY. Hold the value in a component field and copy it into the class when you save it, or make the row
a real entity.
```

Take the first of those two ways out and the whole thing works, because of the half above: **bind an ordinary
component field, then copy it into the class on save** — the copy is an in-place write, so the derived total moves
with it.

```osy title="✓ bind a component field, copy it into the class on save" test app=ui-reactivity
[Page("/picks/edit")]
[AllowAnonymous]
[Render(CSR)]
component PickEditor() {
  List<Pick> picks = [ new Pick("Ann"), new Pick("Bo") ];

  int draft = 1;      // an assignable component field — this is what the control binds to
  Pick editing;       // which class value the draft is destined for

  int Total() { int t = 0; foreach (var p in picks) { t = t + p.Weight; } return t; }

  action Edit(Pick p) { editing = p; draft = p.Weight; }
  action Save() { editing.Weight = draft; editing = null; }   // the copy — an in-place write, so the screen moves

  render {
    Stack(gap: 3) {
      foreach (var p in picks) {
        Row(gap: 2) { Text(p.Name); Text("w=" + p.Weight); Button("Edit " + p.Name, onPress: () => Edit(p)); }
      }
      if (editing != null) {
        Row(gap: 2) { NumberField("Weight", value: draft); Button("Save", onPress: () => Save()); }
      }
      Text("Total shares: " + Total());
    }
  }
}

[Test]
void a_component_field_carries_the_edit_into_the_class() {
  Ui.Visit("/picks/edit");
  Assert.Visible("Total shares: 2");
  Ui.Click("Edit Ann");
  Ui.Fill("Weight", "4");
  Ui.Click("Save");
  Assert.Visible("Total shares: 5");
}
```

**So pick the shape by how the value is produced, not by how it is stored.** A class value is an excellent carrier
for anything **derived** — a computed balance, a settlement transfer, a running subtotal, a
[projected row](#projected) — because those are written by your own code and read by the render, which is exactly
the half that works. It is the wrong carrier for anything a person **edits directly through a control**: there, either
keep the edited scalar in a component field and copy it across as above, or make the row a real
[entity](https://osysharp.com/reference/entity/declaration/) and bind to that. Both are ordinary; neither needs the list reassigned.

### Automatic disposal — the ease of `live` without the leak   {#disposal}
Every reactive thing a region creates — a live query's subscription, an `on change` reaction, a child component — is
**owned by that region's scope**. When the region leaves (a `foreach` row drops, an `if` branch flips, the page closes),
its scope disposes and takes all of that down with it, children-first. You never write the un-subscribe; forgetting to
stop a live-ness is not a bug you can have here.

## Examples       {#examples}
All four kinds on one page — a declaration feeds the render *and* a reaction; `on mount` seeds; `on unmount` closes out:

```osy title="the-whole-model" test app=ui-reactivity
entity Organization { string Name; }

[Page("/org/new")]
[Render(CSR)]
component OrgCreate() {
  Organization draft;
  on mount { draft = new Organization {}; }          // once, before paint — seed the editable ghost

  live var tabName = draft?.Name ?? "New organization";   // a live declaration — recomputes as you type
  on change { Navigation.SetTitle(tabName); }        // a reaction — re-runs only when tabName changes
  on unmount { Log.Information("create form closed"); }    // once, at teardown

  render {
    Stack(gap: 4) { Input(value: draft.Name, placeholder: "Organization name"); }   // the text slot tracks draft.Name
  }
}
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the member table this model underlies.
- [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) — `on mount` / `on unmount` in full.
- [on change](https://osysharp.com/reference/ui/on-change/) — the tracked reaction, in full.
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — how a seeded `new Entity{}` ghost rides the page overlay and `UnitOfWork.Commit()`.
- [Classes](https://osysharp.com/reference/class/index/) — what a class value is, and why an edit through a list index sticks ([[class-index#reference]]).


---

<!-- https://osysharp.com/reference/ui/validation/ -->

# Validation

> You declare a field's rules once, on the entity — `[Required]`, `[Pattern]`, `[MaxLength]` — and give each rule the sentence to show when it fails. A bound `Input` then configures itself from them, the browser marks a bad value invalid, a save that cannot succeed is refused before it leaves the page, and a save the server refuses raises a `ValidationException` your action can catch and place beside the offending field.

<!-- id: ui-validation · area: ui · stability: stable · html: https://osysharp.com/reference/ui/validation/ -->

## Summary        {#summary}
The rules live on the **entity**, once, together with the words to say when they're broken:

```osy syntax
entity Organization {
  [Required("A name is required."), MaxLength(255)] string Name;
  [Required("A slug is required."),
   MaxLength(100),
   Unique,
   Pattern("^[a-z0-9]+(-[a-z0-9]+)*$", "Use lowercase letters, numbers and hyphens — like acme-corp.")]
  string Slug;
}
```

Everything else follows from that. The form doesn't restate the rule, so it can't drift from it.

## Signature      {#signature}
```osy syntax
[Required("message")]                  // …the message is optional on every rule
[Pattern("regex", "message")]
[MaxLength(100, "message")]
[MinLength(3, "message")]
[Min(0, "message")] [Max(10, "message")]

catch (ValidationException ex) {
  ex.Message      // the sentence(s) — what to show if you only want one line
  ex.Violations   // one Violation per refused field: .Entity .Field .Rule .Message
}
```

## Description    {#description}
Four things happen, and you author only the last one.

**1. The control obeys the model.** `Input(value: draft.Slug)` already names the property it edits, so it configures
itself from that property's rules — `required`, `maxlength`, `minlength`, `pattern`, `min`, `max` become real
attributes on the field. The browser enforces what it can (you cannot type past a `maxlength`) and judges the rest, so
a bad value is genuinely invalid, not "invalid according to some code we wrote twice".

**2. Your design system paints it.** A field that fails its rules is in the `Invalid` state — an ordinary state in a
`variants` block, next to `Hover` and `Focus` — so you style it **once** and every control agrees:

```osy syntax
variants { base { … Invalid { Border = FillDanger; } } }
```
It holds until the user has actually touched the field, so a blank required field is not red before it's been filled in.

**3. A doomed save never leaves the page.** `UnitOfWork.Commit()` checks the pending changes against the model first. If they
can't be accepted, it raises straight away — no round-trip, and the message appears instantly.

**4. A refused save says why — where it happened.** Whether it was refused locally or by the server, it raises the same
`ValidationException`, carrying the same `Violations`. Catch it and put each message beside the control that produced
it.

**The client is being polite, not standing guard.** The server validates every write it receives, and it has the last
word — a slug someone else claims between your check and your save is refused *there*. The local check is deliberately
conservative: it never accuses a field the user hasn't filled in (that field may have a default only the server knows),
and it never tries to answer `Unique`, which is a question about *other* rows. When in doubt it stays quiet and lets
the write go, because wrongly blocking a good save is worse than a late "no".

**The platform never writes the words.** It reports which field broke which rule and hands you the sentence *you*
declared beside it. A regex is not something to show a human, so if you word nothing, nothing is worded.

### Where does the draft live?   {#draft-scope}
Everything above rests on the draft being an **entity**: `Input(value: draft.Slug)` can configure itself from
`Slug`'s rules only because `draft` is an `Organization`, and `[Required("A slug is required.")]` is the only place
that sentence is written. Hold the fields as loose `string`s instead and both halves go: the control has no property
to read rules from, and each sentence has to be re-typed as a guard in the action — the same words in two files, free
to drift. So **keep the entity-typed draft**.

There is exactly one shape where it bites, and it is worth knowing before you meet it: **a component that holds a
draft AND queries the same entity**. A draft is pending from the moment the component mounts, and the page's own
queries read pending rows back — so the list paints a blank phantom row, and the never-filled draft rides the next
genuine save, failing on a `[Required]` for a row the user never opened. `osy lint` reports that pair as
`ui-draft-field-ghosts-its-own-list` (SHOULD) and names both members.

**The fix that keeps this page's guarantee** is not to give up the entity — it is to give the draft its own unit of
work, by putting it on a component you open with `Dialog.Open(…, unitOfWork: Root)`. Nothing is then pending in the
list's unit of work, and the rules and their sentences stay declared exactly once:

```osy title="the draft on its OWN unit of work — the list stays clean, the messages stay declared once" test app=ui-validation-scope
entity Organization {
  [Required("A name is required."), MaxLength(255)] string Name;
}

[Render(CSR)]
component NewOrganization() {
  Organization draft = new Organization {};      // pending in THIS component's unit of work, not the page's

  action Save()   { Dialog.Confirm(); }          // a Root dialog's Confirm is what persists it
  action Cancel() { Dialog.Discard(); }

  render {
    Stack(gap: 2) {
      Input(value: draft.Name, placeholder: "Organization name");
      Button("Cancel", onPress: Cancel);
      Button("Save", onPress: Save);
    }
  }
}

[Page("/orgs")]
[Render(CSR)]
component OrgListPage() {
  live var orgs = Organization.ToList();         // no draft here, so no phantom row
  action New() { Dialog.Open(NewOrganization(), unitOfWork: Root); }
  render {
    Stack(gap: 2) {
      foreach (var o in orgs) { Text(o.Name); }
      Button("New organization…", onPress: New);
    }
  }
}
```

Local scalars are the other way out the linter offers, and they are the right answer when the form's fields do not
correspond to one entity at all. Reach for them knowing the cost: **the declared sentence is gone**, and you write
`if (title == "") { formError = "A name is required."; return; }` for each rule you had declared. See
[[ui-data-mutation#draft-scope]].

## Examples       {#examples}
The whole loop — rules on the entity, messages beside the fields that broke them. This page holds a draft and
**no query over `Organization`**, which is the shape the linter is looking for the absence of:

```osy title="org-form" test app=ui-validation
entity Organization {
  [Required("A name is required."), MaxLength(255)] string Name;
  [Required("A slug is required."),
   Pattern("^[a-z0-9]+(-[a-z0-9]+)*$", "Use lowercase letters, numbers and hyphens — like acme-corp.")]
  string Slug;
}

[Page("/org/new")]
[Render(CSR)]
component OrgCreatePage() {
  Organization draft = new Organization {};   // the draft IS the field — nothing to fetch at mount
  string nameError = "";
  string slugError = "";

  action Create() {
    nameError = "";
    slugError = "";
    try {
      UnitOfWork.Commit();
      Navigation.Close("/org/new", true);
    }
    catch (ValidationException ex) {
      foreach (var v in ex.Violations) {
        if (v.Field == "Name") { nameError = v.Message; }
        else if (v.Field == "Slug") { slugError = v.Message; }
      }
    }
  }

  render {
    Stack(gap: 4) {
      Input(value: draft.Name, placeholder: "Organization name");
      if (nameError != "") { Text(nameError); }

      Input(value: draft.Slug, placeholder: "team-slug");
      if (slugError != "") { Text(slugError); }

      Button("Create", onPress: Create);
    }
  }
}
```

Only need one line, not per-field placement? `ex.Message` is the sentences, joined:

```osy title="one line instead of per-field placement" syntax
action Save() {
  try { UnitOfWork.Commit(); }
  catch (ValidationException ex) { error = ex.Message; }
}
```

## See also       {#see-also}
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — `UnitOfWork.Commit()`, binding an input to an entity field, and where a draft belongs.
- [Dialog.Open / Dialog.Ask / Dialog.Confirm / Dialog.Discard](https://osysharp.com/reference/ui/dialogs/) — `Dialog.Open(…, unitOfWork: Root)`, the component whose unit of work is its own.
- [debounce](https://osysharp.com/reference/ui/debounce/) — asking the server a question as you type (the is-this-name-taken check), which `Unique` cannot answer locally.
- [style props](https://osysharp.com/reference/ui/styling/) — `variants`, and the states a control can be in.
- [Navigation](https://osysharp.com/reference/ui/navigation/) — `Navigation.Save`, which can be refused the same way and caught the same way.


---

<!-- https://osysharp.com/reference/ui/visitor/ -->

# Visitor

> `Visitor.Id` is a stable opaque id for the browser someone is using, minted on their first visit and remembered afterwards. It gives work started before sign-up — a shop's basket, a saved filter — somewhere to belong, and a key to hand a real owner once there is one. It is a name, never a credential: it arrives from the browser, so it proves nothing and must never gate access to anything.

<!-- id: ui-visitor · area: ui · stability: stable · html: https://osysharp.com/reference/ui/visitor/ -->

## Summary        {#summary}
Plenty of what a person does happens **before they are anybody**. They browse a catalogue, fill a basket, set a filter
— and only then, if at all, do they sign up. `Session.CurrentUser` is null for all of it, which is correct and no help:
that work still has to live somewhere, and still has to be there after a refresh.

**`Visitor.Id`** is the missing name. It is a stable opaque id for this browser, minted the first time it is read and
remembered across visits, so an app can key rows to *the visit that created them* and hand them a real owner later.

```osy title="a basket keyed to the visit" test app=ui-visitor
[Page("/cart")]
[AllowAnonymous]
[Render(CSR)]
component CartPage() {
  string visitId = Visitor.Id;                 // who this browser is, signed in or not
  live var cart = Cart.Include(c => c.Lines).SingleOrDefault(c => c.VisitId == visitId);
  render {
    if (cart != null) { foreach (var l in cart.Lines) { Text(l.Product.Name); } }
  }
}
```

**What this needs around it:** a `Cart` entity carrying the visit's `VisitId` and a `Lines` collection, a `CartLine`,
and a `security` block on each — the id arrives from the browser, so the entity's own grants are the whole
protection. [The example below](#examples) declares all of it as one compiled app.

> ⚠ **A name, not a credential.** `Visitor.Id` comes from the browser, so it is *input*, never evidence. It identifies
> a visit; it authorizes nothing. Rows keyed by it are protected by your entity's `security` block and by nothing else
> — see [what to assume about the id](#security) below.

## Signature      {#signature}
```osy syntax
Visitor.Id     // string — a stable opaque id for this browser. Never empty.
```

Available in every component, with no `using`. Reading it is what mints it.

## Description    {#description}

### How stable is `Visitor.Id`? — same browser, same value   {#what}
The **same** value on every page of the app and after a reload, in this browser. A **different** value in another
browser, another profile, or a private window — and a **new** one if the visitor clears their site data, which is what
"forget me" should mean.

It is not derived from anything about the person: not their address, not their device, not their behaviour. It is an
opaque id, generated at random, which is exactly why it can be handed out freely — it says only *"this is the same
browser as before"*, which is the entire question an app needs answered.

### It is a CLIENT value   {#client-side}
`Visitor.Id` lives in the browser, so an expression reading it runs client-side, like every other ambient. To use it on
the server, **pass it as an argument**:

```osy title="pass it to the server as an argument" test app=ui-visitor
[Page("/products/{slug}")] [AllowAnonymous] [Render(CSR)]
component ProductPage(string slug) {
  string visitId = Visitor.Id;
  var product = Product.SingleOrDefault(p => p.Name == slug);
  action Add(Product p) { AddToCart(visitId, p); }     // the server takes it as input
  render {
    if (product != null) { Pressable(onClick: () => Add(product)) { Text("Add to cart"); } }
  }
}
```

This is not a limitation to route around — it is the honest shape. A client-supplied id is something the server should
receive and treat as input, never something it should quietly trust as identity.

### Moving a visitor's rows to their account on sign-in   {#claiming}
The point of a visit-keyed row is that it becomes an owned one. When the visitor signs in, the app moves the rows it
cares about from the visit to the user — the handover it exists for:

```osy title="handing a visit's rows a real owner" test app=ui-visitor
void ClaimCart(string visitId, User owner) {
  var cart = Cart.SingleOrDefault(c => c.VisitId == visitId && c.Owner == null);
  if (cart != null) { cart.Owner = owner; }
}
```

Whether to claim, merge, or discard when the user already has rows of their own is the app's decision, and the platform
has no opinion: it supplies the id and nothing else.

### Can a visitor forge the id? — what to assume   {#security}
Rows keyed by `Visitor.Id` are exactly as protected as your entity says they are. Because the id arrives from the
browser, a visit-keyed entity is one where the app should think about the `security` block rather than reach for the
defaults — the same way it would for anything an anonymous caller can reach.

Two rules keep this simple:

- **Never gate anything that matters on a visitor id.** It is not a login. It cannot stand in for one.
- **Never put anything in a visit-keyed row that would harm the visitor if another visitor read it.** A basket of
  product references is the right size of thing; a saved address is not, until there is a user to own it.

Pages that a visitor reaches before signing in must also declare [`[AllowAnonymous]`](#see-also) — routed components
require an authenticated principal by default, which is what makes reaching for `Visitor.Id` a deliberate act rather
than something an app drifts into.

⚠ **And so must every FUNCTION those pages call.** A page being public settles who may SEE it; who may CALL a
function is a separate grant on the function, and the two are easy to conflate — this page's own example did, until
2026-08-25. Without it the server refuses the hand-off, the call does nothing, every statement after it in that body
is unwound, and **nothing appears on screen to say so**: the visitor presses "Add to cart" and the basket stays
empty. The compiler now refuses that shape and names both fixes.

### When storage is unavailable   {#no-storage}
Some browsers refuse site storage (a privacy mode, an embedded webview). There `Visitor.Id` still returns a stable id
for as long as the page is open, so nothing breaks and nothing throws — it simply is not remembered after a reload. An
app that wants to notice can: a basket that comes back empty is the visitor's answer.

## Examples       {#examples}
An anonymous basket — the whole shape, from an empty visit to a claimed cart:

```osy title="visitor-cart" test app=ui-visitor
[Principal] entity User { string Email; }

entity Product {
  [Required, MaxLength(80)] string Name;
  decimal Price;
  security { allow read when IsAnonymous || IsAuthenticated; }   // a catalogue is public — that is what a shop is
}

entity Cart {
  // ⚑ NAMED FOR WHAT IT IS. `Visitor.Id` is a NAME the browser supplies — it identifies a visit and authorizes
  //    nothing (see "A name, not a credential" above), so calling the column `Token` reads as a secret to every
  //    reader, the linter included.
  [Required, MaxLength(64)] string VisitId;    // the visit this basket belongs to
  User Owner;                                  // null until someone signs in and claims it
  [ForeignKey(Cart)] CartLine[] Lines;
  // ⚠ Reachable by anyone, signed in or not — see Security above. That is the honest cost of a row keyed by
  // something the browser supplies, and the reason a basket holds product references and nothing else.
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

entity CartLine {
  [Required] Cart Cart;
  [Required] Product Product;
  int Quantity = 1;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

// The server takes the id as an ARGUMENT — it is input from the browser, never identity.
[AllowAnonymous]
void AddToCart(string visitId, Product product) {
  var cart = Cart.SingleOrDefault(c => c.VisitId == visitId);
  if (cart == null) { cart = new Cart { VisitId = visitId }; }
  var line = new CartLine { Cart = cart, Product = product };
  UnitOfWork.Commit();      // adding to a basket is a complete act — nothing else is going to commit it
}

[Page("/")]
[AllowAnonymous]
[Render(CSR)]
component Catalog() {
  string visitId = Visitor.Id;
  live var products = Product.OrderBy(p => p.Name).ToList();

  action Add(Product p) { AddToCart(visitId, p); }

  render {
    Stack(gap: 3) {
      foreach (var p in products) {
        Row(gap: 2) {
          Text(p.Name);
          Button("Add to bag", onPress: () => Add(p));
        }
      }
    }
  }
}
```

## See also       {#see-also}
- [routes and pages](https://osysharp.com/reference/ui/routing/) — protected-by-default routing, and the `[AllowAnonymous]` a visitor-facing page must declare
- [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) — what a real authorization gate looks like, and why a visitor id is not one
- [component](https://osysharp.com/reference/ui/component/) — where an ambient is read, and how a component holds it in a field


---

<!-- https://osysharp.com/reference/ui/content-security-policy/ -->

# What an app page is allowed to load

> Every app page is served with a Content-Security-Policy the browser enforces. Scripts, styles, fonts, workers and data connections must come from the app's own origin; images may additionally come from any `https:` address. A control that tries to reach a third-party origin is refused by the browser, silently — so a vendored library must ship the parts it needs rather than fetch them at run time.

<!-- id: ui-content-security-policy · area: ui · stability: preview · html: https://osysharp.com/reference/ui/content-security-policy/ -->

## Summary        {#summary}
An app page is not an open document. It is served with a **Content-Security-Policy**, and the browser — not the
platform — enforces it. The shape is *same-origin by default*: the client, every control bundle, every chunk, every
font and every data connection comes from the app's own address.

Two deliberate widenings make ordinary content work. **Images may come from any `https:` address**, so a document that
embeds a remote picture renders. And **inline styles are allowed**, because that is how the renderer and every
editor-style control position things.

The rule with teeth for a control author is the one about *fetching*: **a control may not load code, data or a font
from a third-party origin.** Vendor what you need — see [chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/), which exists so a library can ship its
own parts, including a worker and a `.wasm` sibling, and still be served from your app. WebAssembly compiled from
something your app served is allowed; a module fetched from someone else's origin never gets that far.

## Description    {#description}

### What the policy allows   {#allowed}

| What | Allowed from | Why it is drawn there |
|---|---|---|
| **Scripts** | the app's own origin, plus one hashed inline bootstrap | the client, control bundles and chunks are all served by your app. There is no `unsafe-inline` and no `unsafe-eval` |
| **WebAssembly** | may be compiled, from a module the rules above let you fetch | a control may ship a `.wasm` chunk, and a browser refuses to compile one unless the policy says so. It permits compiling a module and nothing else — no string ever becomes JavaScript |
| **Styles** | the app's origin, and **inline** | the renderer paints style attributes, and controls position themselves with them continuously |
| **Images** | the app's origin, **any `https:` address**, and `data:` | a document that embeds a remote image is ordinary content, and stylesheets legitimately draw small icons as `data:` SVG |
| **Fonts** | the app's origin, and `data:` | your app's web fonts are served by your app; a chunk's stylesheet may inline a face |
| **Connections** (fetch, WebSocket) | the app's origin | the data channel and live updates are all your app's own address |
| **Workers** | the app's origin | a package chunk may start one; it is served from your app like the rest of the package |
| **Frames** | nothing, in either direction | no page embeds another, and no page may be embedded — which is also what stops clickjacking |

Plugins (`object`), and a `<base>` element that could re-point every relative URL on the page, are refused outright.

Two companion headers ride along: responses are marked `nosniff`, and the referrer sent to another site is trimmed to
your origin — so following a link out of a document does not hand the other site the record id in your page's address.

### The failure is SILENT — this is the part worth remembering   {#silent-failure}

A refused subresource does not raise an error your code can catch. The browser simply does not fetch it, writes a line
to the console, and carries on. A feature that lazily loads something therefore does not *break* — it quietly does
nothing, which looks like a bug anywhere except where it is.

So when a control works in isolation and does nothing in an app, **open the browser console first**. A CSP refusal
names the directive and the address it blocked:

```text
Refused to load the script 'https://cdn.example.com/lib.js' because it violates
the following Content Security Policy directive: "script-src 'self' 'sha256-…'".
```

### What this means when you ship a control   {#controls}

**Vendor, do not fetch.** A library that hard-codes a CDN address for its own code will be refused. The answer is to
ship it as part of your control, which is what a chunk **package** is for: a directory served under a real base, so the
library's own relative imports, `new Worker(new URL(…))` and `.wasm` siblings all resolve exactly as they would on a
static host. See [chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/).

**WebAssembly vendors cleanly too, and this is where people expect trouble.** A `.wasm` is an ordinary chunk: it is
served from your app with the right type, and the policy admits compiling it. The one thing to watch is that
Emscripten-built libraries do not embed their module — they *ask* for its address at run time, and the default answer
is usually a CDN, which will be refused. Point that lookup at your own chunk instead. `Osysharp.Barcode`
([The barcode kit — a QR and barcode scanner you opt into](https://osysharp.com/reference/ui/barcode-kit/)) is the worked example: a 1.07 MB ZXing decoder reached through `host.chunkUrl("Wasm")`.

That covers more than it sounds like it should — a diagram engine that lazy-loads a renderer per diagram type, or a
speech model's WASM runtime, both vendor cleanly. What it does not cover is a library that must reach *its own*
origin at run time, such as one fetching multi-hundred-megabyte model weights from a CDN. There is no way to declare
that today; the page's policy refuses it, and that is the current answer rather than an oversight.

**Remote images are fine.** `![](https://…)` in a document, or an image whose address is data, renders normally. Only
`http:` addresses are refused, and those would be blocked as mixed content on a secure page anyway.

**Inline styles are fine.** Setting a `style` attribute from a control works, as does anything the renderer paints.

## See also       {#see-also}
- [chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/) — how to ship a library's own parts so it is served from your app rather than fetched
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — declaring and mounting a control
- [The markdown editor kit — a rich editor you opt into](https://osysharp.com/reference/ui/markdown-editor-kit/) — a worked example of a vendored, chunked control


---

<!-- https://osysharp.com/reference/ui/csharp-differences/ -->

# Writing a component — what differs from C#

> Osy# is C# almost everywhere, which is what makes the handful of deliberate differences worth knowing before you hit them. Ordinary C# works and should be written directly — switch expressions, `Math.Floor`/`Math.Ceiling`, `Room.Members.Length`, and `p.Name?.Trim() ?? "none"` all compile, in a function and in a `render` block alike. Inside a component three things differ: there is no `method` keyword (a method is a return type and a name, exactly as in C#), `public` is a `class` modifier and not an entity one, and a reactive side effect is `on change { … }` rather than a named `effect`. Each of these produces a clear compile error — this page is so you meet them here first.

<!-- id: ui-csharp-differences · area: ui · stability: stable · html: https://osysharp.com/reference/ui/csharp-differences/ -->

## Summary        {#summary}

Osy# is C#, and the goal is that your C# instincts are right. They almost always are — which is exactly why the few
places they are **not** cost more than their number suggests: you write the C# form, and only a failed compile tells
you otherwise.

Three of those live in and around a `component`. All three produce a clear error naming the replacement, so nothing
here is a trap you can ship — but reading them once is cheaper than meeting them one failed compile at a time.

## Description    {#description}

### Ordinary C# that works — write it, do not route around it        {#works}

| Write this | Where |
|---|---|
| `r switch { Room.Kitchen => 1, Room.Bedroom => 2, Room.Bath => 3 }` | a function, a computed, a `render` block |
| `Room.Members.Length` — and `Enum.GetValues<Room>()` is the same array | anywhere |
| `Math.Floor(x)` · `Math.Ceiling(x)` · `Math.Round` · `Math.Abs` · `Math.Min` · `Math.Max` | anywhere |
| `p.Name?.Trim() ?? "none"` — null-conditional and null-coalescing, chained | anywhere |
| `x ??= fallback` · ternaries · `foreach` · `var` · string interpolation · LINQ | anywhere |

Write the C# form first. A construct Osy# does not take is a compile error naming the line and the replacement.

```osy title="ordinary C#, compiled" test app=ui-csharp-differences
enum Room { Kitchen, Bedroom, Bath }

entity Kiln {
  [MaxLength(80)] string Name = "";
  Room Where = Room.Kitchen;
  decimal Litres = 0m;
  security { allow read, create, update, delete when IsAnonymous || IsAuthenticated; }
}

int Rank(Room r) => r switch { Room.Kitchen => 1, Room.Bedroom => 2, Room.Bath => 3 };

int RoomCount() { return Room.Members.Length; }

decimal WholeLitres(Kiln p) { return Math.Floor(p.Litres); }

string Caption(Kiln p) { return p.Name?.Trim() ?? "unnamed"; }
```

### There is no `method` keyword        {#no-method-keyword}

A component method is a **return type and a name**, exactly as in C#. The token after the name is what distinguishes
a method from a field.

```osy syntax
component ReportRow(Report report) {
  string Load() { … }        // ✓ a method — return type, name, parameter list
  method string Load() { … } // ✗ `method` is not a keyword
}
```

`method` is the word most people try, because the surrounding declarations (`action`, `on change`) *do* read as
keywords. They are different things: `action` and `on change` name reactive machinery that has no C# equivalent, so
they get a word. A method is just a method, so it looks like one.

See [component](https://osysharp.com/reference/ui/component/) for the full member list.

### `public` is a `class` modifier, not an entity one        {#public-on-a-field}

This is the difference most likely to bite, because the two forms sit a line apart and look alike:

```osy syntax
class ReceiptFields {
  public decimal? Amount;   // ✓ a class member IS private by default — exactly C#
}

entity Report {
  public string Title;      // ✗ refused
  string Title;             // ✓
}
```

A `class` is an ordinary C# type: its members are private by default and `public` opens them. An **entity** is not —
its fields are a data surface, and who may read or write them is not a property of the field but a declared rule on
the entity ([security { }](https://osysharp.com/reference/security/entity-security/)). Access control there is the `security { }` block, and allowing `public` on
a field would suggest a second, weaker answer to a question that already has one.

The compiler says so directly:

> Visibility/`const` modifiers apply to `class` members; entity access control is the `security { }` block.

Note this is about the FIELD. Entity and class *types* do take a visibility modifier, and their defaults differ —
see [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/).

### A reactive side effect is `on change { … }`, not a named `effect`        {#on-change}

There is no `effect` keyword. A block that reacts to its dependencies changing is `on change`, one of the lifecycle
family alongside `on mount` and `on unmount`:

```osy syntax
component Search(string term) {
  on change { … }           // ✓ runs when what it reads changes
  effect Watch { … }        // ✗ `effect` is not a keyword
}
```

Writing the old form gets an error that names the replacement and the rest of the family, so you land in the right
place from the first attempt. [on change](https://osysharp.com/reference/ui/on-change/) covers when it runs and what it depends on; [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) covers
`on mount` / `on unmount`.

## Examples       {#examples}

All three correct at once — a method declared C#-style, a `class` whose members take `public`, and an entity whose
fields do not. This one is compiled by the documentation build, so it is the shape to copy:

```osy title="all three, correct" test app=ui-csharp-differences
class Filters {
  public string? Term;               // class member: private by default, `public` opens it
}

entity Report {
  [MaxLength(200)] string Title = "";   // entity field: no visibility modifier
}

[Page("/reports")]
[AllowAnonymous]
component ReportList() {
  var filters = new Filters();

  string Caption() {                 // a method — a return type and a name, no `method` keyword
    return filters.Term == null ? "All reports" : "Filtered";
  }

  render { Text(Caption()); }
}
```

## See also       {#see-also}

[component](https://osysharp.com/reference/ui/component/) — the component archetype and every member kind it can hold.

[on change](https://osysharp.com/reference/ui/on-change/) — the reactive side-effect block, and what makes it re-run.

[on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) — `on mount` and `on unmount`.

[type visibility (public / internal)](https://osysharp.com/reference/types/visibility/) — visibility on TYPES (where entity and class defaults genuinely differ).

[class methods](https://osysharp.com/reference/class/methods/) — methods on a `class`, which follow the same shape as a component's.


---

<!-- https://osysharp.com/reference/ui/composable/ -->

# [Composable] — presentational components in public pages

> Mark a presentational, composition-only component `[Composable]` so a public page can compose it without marking it `[AllowAnonymous]` itself. A composable component carries no auth identity — it inherits its render surface from whoever composes it — and it ships bundled with its parent, so an anonymous visitor never triggers a separate blocked fetch for it. The data path is unaffected: every query still gates on the queried entity.

<!-- id: ui-composable · area: ui · stability: stable · html: https://osysharp.com/reference/ui/composable/ -->

## Summary        {#summary}
UI is **secure by default**: a component is protected unless you explicitly make it public with
[[ui-authorize|`[AllowAnonymous]`]]. That works for **entry points** — routed pages and trees a client fetches
directly. But a **library of presentational components** (a `Card`, a `Hero`, a `NavBar`) is composed *inside*
pages and carries no data or identity of its own. Marking each one `[AllowAnonymous]` would be noise, and forgetting
one would break an otherwise-public page.

**`[Composable]`** is the marker for exactly those components:

```osy syntax
[Composable]
component Card(string title) {
  render { Box { Text(title); } }
}
```

A `[Composable]` component **inherits its render surface from whoever composes it**. A public page can render it,
an anonymous visitor can receive it (bundled with the page), and you never mark it `[AllowAnonymous]`.

## Signature      {#signature}
```osy syntax
[Composable] component Card(...) { ... } — a composition-only component with no auth identity
```

## Description    {#description}
`[Composable]` says one thing: *"I am presentational and composition-only — I carry no auth identity."* Two
consequences follow, and one hard rule stays untouched.

- **It is anon-composable.** A public (`[AllowAnonymous]`) page may compose a `[Composable]` component and
  server-render it inline; an anonymous client may receive its structure.
- **A public page composing an unmarked component is a COMPILE ERROR**, naming the component and both attributes
  that would fix it. An anonymous visitor is never served the structure of a component that is neither
  `[AllowAnonymous]` nor `[Composable]` — so a page that composes one could not paint for the very visitors it was
  marked public for. The check follows composition all the way down: a `[Composable]` that itself composes an
  unmarked component is the same problem one hop further out, and the error says which path reaches it.
- **It ships bundled.** When a page composes `[Composable]` components, they travel *with* the page's payload, so
  an anonymous visitor's browser never issues a separate request for each child (which would be blocked for a
  protected component). Composition is seamless and needs no per-child round-trip.
- **The data path never inherits (hard rule).** `[Composable]` only concerns a component's *structure*. Every
  query a component runs is still gated on the queried entity's own rules, exactly as anywhere else — a composable
  component with no data of its own exposes nothing. See [component](https://osysharp.com/reference/ui/component/).

Use `[Composable]` for presentational library components. Use `[AllowAnonymous]` for a page or a directly-fetched
tree you intend to be public. They are independent: a component is neither by default (secure), and the two flags
answer different questions ("is this a public entry point?" vs. "is this a presentational piece I compose?").

## Examples       {#examples}
A public page composing a composable card — no `[AllowAnonymous]` on `Card` needed:

```osy title="a composable, and a page that composes it" test app=ui-composable
[Composable]
component Card(string title) {
  render { Box { Text(title); } }
}

[Page("/")]
[AllowAnonymous]
component Home() {
  render {
    Stack(gap: 2) {
      Card(title: "Welcome");
      Card(title: "Get started");
    }
  }
}
```

## See also       {#see-also}
- [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) — `[AllowAnonymous]` / `[Authorize]` for pages and entry points.
- [component](https://osysharp.com/reference/ui/component/) — declaring components and how composition works.
- [Slot (child content)](https://osysharp.com/reference/ui/slots/) — projecting child content into a composed component.


---

<!-- https://osysharp.com/reference/ui/accessibility/ -->

# accessibility

> Tags already give an element its role, focus and keyboard behaviour. The semantic props say the rest: `role:` for a widget a tag cannot name, `checked:`/`selected:`/`expanded:`/`sort:` for the state it is in, `label:` for what it is called, and `decorative:` for what should not be announced at all.

<!-- id: ui-accessibility · area: ui · stability: stable · html: https://osysharp.com/reference/ui/accessibility/ -->

## Summary        {#summary}

A control has to be usable by someone who cannot see it. Most of that you get for free; the rest is one small
vocabulary of props.

```osy syntax
Pressable(onClick: Toggle, role: UiRole.Checkbox, checked: on, label: "Email me") {
  Row(align: Align.Center, gap: 2) { Box(w: "18px", h: "18px"); Text("Email me"); }
}
```

Rendered, that is a real `<button role="checkbox" aria-checked="true" aria-label="Email me">`. Without the three
props it is a button that draws a filled square — which says everything to someone looking at it and nothing to
anyone else.

## Signature      {#signature}

```osy syntax
<element>(role: <UiRole>, label: <string>,
          checked: <bool>, selected: <bool>, expanded: <bool>, current: <bool>, invalid: <bool>,
          sort: <UiSort>, decorative: <bool>)

// …and the ASSOCIATION props, whose value is a compile-scoped HANDLE, not a value:
<element>(id: <handle>, labelFor: <handle>, labelledBy: <handle>, describedBy: <handle>)
```

Every one is **ambient**: it works on any element, because any element can be a control. A `Box` can be a dialog and
a `Row` can be a tab.

## Description    {#description}

### What you already have, free   {#built-in}

Start here, because it is more than it sounds. The renderer picks real HTML tags, and the tag carries the semantics:

| you write | you get | which means |
|---|---|---|
| `Pressable` / `Button` | `<button>` | announced as a button, focusable, activates on Enter **and** Space |
| `Link(href:)` | `<a>` | announced as a link, in the page's link list |
| `Input(value:)` | `<input>` | announced as an entry field, works with every assistive technology |
| `disabled: true` | the native `disabled` attribute | announced as unavailable, and it stops its own events |
| `keys: [Left, Right]` | `tabindex` + key ownership | a custom surface reachable by keyboard at all — see [keys](https://osysharp.com/reference/ui/keys/) |

So a page built from these is already keyboard-operable and already announces its structure. **You do not write ARIA
for any of that**, and adding it would only be a second, staler copy of what the tag already says.

### What a tag cannot say   {#gaps}

Three things, and they are the three that go missing:

**STATE.** A tag cannot say a checkbox is ticked, a tab is the current one, a dropdown is open, or a column is the
one being sorted. Apps draw those — a fill, an underline, a chevron, an arrow — and drawing is not saying.

**NAME.** A button whose face is an icon has no text to be announced by. The icon itself is hidden from assistive
technology (correctly — it is a picture of the meaning, not the meaning), so the control is announced as "button",
with nothing to distinguish it from every other button on the page.

**ASSOCIATION.** A label rendered above an input is, to anyone reading visually, obviously that input's label. To
anyone else it is a piece of text that happens to be nearby — and clicking the words does nothing, where in an
ordinary form it focuses the field. Same for the red message under a rejected value: it says why, to whoever can see
that it belongs to that field.

### Which prop says what — role, state, label   {#props}

#### `role:` — what this element IS   {#role}

Takes a member of the closed `UiRole` vocabulary. It is not a string, so a typo is a compile error naming the
members that exist rather than an attribute every browser silently ignores.

**Write it qualified — `role: UiRole.Tab`, never a bare `Tab`.** In an argument slot a bare capitalised name could be
a theme token, an enum member or a style keyword, so the group name is what says which vocabulary you meant. The
member tables below name the members; the value you write is always `UiRole.` + the member. The same holds for
`sort:`, whose vocabulary is `UiSort`.

```osy title="say which vocabulary you meant — UiRole.Tab, never a bare Tab" syntax
Row(role: UiRole.TabList) { … }
Pressable(role: UiRole.Tab, selected: isCurrent, onClick: Show) { … }
Box(role: UiRole.Dialog, label: title) { … }
```

The members, grouped by what they are for:

| group | members |
|---|---|
| toggles and choice | `Checkbox` `Switch` `Radio` `RadioGroup` `Tab` `TabList` `TabPanel` `Option` `ListBox` `ComboBox` `Menu` `MenuItem` |
| overlays and feedback | `Dialog` `Alert` `Status` `Progress` `Tooltip` |
| grouping | `Toolbar` `Group` `Separator` `List` `ListItem` |
| data grid | `Grid` `GridRow` `ColumnHeader` `GridCell` |
| landmarks | `Navigation` `Search` `Banner` `Main` `ContentInfo` `Region` |

`Alert` interrupts a reader immediately; `Status` waits until they are next idle. That is the whole difference
between "your session has expired" and "saved", and it is worth choosing deliberately.

**Landmarks are the cheapest large win on any page shell.** They are the regions a reader jumps *between*, which is
how most people using a screen reader navigate a page they have seen before. A shell that declares them is
skippable; one that does not has to be read from the top every time.

#### Saying it is open, checked or selected   {#state}

| prop | says | goes with |
|---|---|---|
| `checked:` | is it on | `role: UiRole.Checkbox` · `UiRole.Switch` · `UiRole.Radio` |
| `selected:` | is it the chosen one | `role: UiRole.Tab` · `UiRole.Option` |
| `expanded:` | is it open | `role: UiRole.ComboBox` · `UiRole.Menu` |
| `current:` | is this the page you are on | a nav `Link` |
| `invalid:` | is this value rejected | an `Input` |
| `value:` | how far along it is (a **number**) | `role: UiRole.Progress` — the one thing about a bar nobody can see |
| `sort:` | which way is it sorted (`UiSort.Ascending` · `UiSort.Descending` · `UiSort.None`) | `role: UiRole.ColumnHeader` |

**A `role:` without its state is worse than neither.** `role: UiRole.Checkbox` promises a state the element then never
reports, which is invalid and leaves the control unannounced. Ship the pair.

⚠ **`checked:`, `selected:` and `expanded:` are emitted in BOTH directions** — `aria-checked="false"` is the required
way to say "an unticked checkbox". Omitting it says something else entirely: that the element has no checked state at
all. This is the opposite of how `disabled` works, where absence *is* false. You do not have to remember which is
which — write the bool and the platform emits the right thing.

`current:`, `invalid:` and `decorative:` are absent when false, because for those absence is already what the
specification means.

#### `label:` — what it is CALLED   {#label}

A string, and the accessible name. Reach for it whenever the control's face is a glyph, an icon, or content that
came from its caller.

```osy title="an icon has no text to be announced by, so name the button" syntax
IconButton(onPress: Remove, label: "Remove line") { Icon(Icons.Trash); }
```

When an element also renders text, `label:` overrides it — which is what you want for a checkbox whose contents are
a tick *and* a word, and not what you want when the visible text is already the right name.

⚠ **A visible label and an accessible name that disagree break voice control**, where someone says the words they
can see. If both exist, make them the same string.

#### `decorative:` — do not announce this at all   {#decorative}

For an element that carries no information: a marker that repeats a state already reported, or a full-viewport
click-catcher behind a popover.

```osy title="a glyph that only repeats a state already reported" syntax
if (sorted) { Text(desc ? "▼" : "▲", decorative: true); }
```

**This is the right answer for a backdrop, not a dodge.** A dismiss-on-click backdrop is a real `<button>` the size
of the screen; announced, it is an unnamed control in everyone's way, and *naming* it would be worse, because the
accessible way to dismiss an overlay is Escape.

### Writing a control other people will use   {#authoring-a-control}

This is the part that is easy to get half-right, because the two duties fail differently.

**CARRY what the control knows.** A `Checkbox` knows whether it is ticked; a `Tab` knows whether it is selected. The
caller already passed that in, so making them repeat it as an accessibility argument is exactly the drift the control
exists to remove.

**EXPOSE what only the CALLER knows** — which is almost always the name. `IconButton(icon: save)` cannot know whether
it means "Save" or "Save draft". A theme switcher cannot know whether your app says "Theme", "Appearance" or "Dark
mode".

⚑ **A control that hard-codes its accessibility denies its caller the ability to be correct.** A control is a black
box: if it takes no `label`, no app using it can supply one, however much it wants to. That is worse than the gap
itself, because the app cannot route around it.

```osy title="✗ hard-coded accessibility the caller cannot fix, and the ✓ shape" syntax
// ✗ the caller cannot fix this from outside
[Composable] component IconButton(Action onPress) {
  render { Pressable(onClick: onPress) { Slot; } }
}

// ✓ carries what it knows, takes what it cannot know
[Composable] component IconButton(Action onPress, string label = "") {
  render { Pressable(onClick: onPress, label: label) { Slot; } }
}
```

#### The association props — one element POINTING AT another   {#association}

`label:` gives a control a NAME. These give it a RELATIONSHIP: which words label it, and which message describes it.

```osy title="which words label the field, and which message describes it" syntax
Text("Email", labelFor: box);
Input(value: email, id: box, describedBy: note, invalid: error != "");
if (error != "") { Text(error, id: note); }
```

| prop | says |
|---|---|
| `id:` | **declares** a handle — "this element is the one called `box`" |
| `labelFor:` | these words are the label for that element — a real `<label for>` |
| `labelledBy:` | this element is named by that one, when the namer cannot be a `<label>` |
| `describedBy:` | this element is described by that one — a hint, or a validation message |

**The value is a HANDLE, not a string, and never an id.** You write a bare name; the platform mints the actual id.
The handle is scoped to the component, so two components may both use `box`, and one component rendered twenty times
gets twenty distinct ids. A handle declared twice, or a reference naming one that does not exist, is a compile error.

That is the whole reason it is not a string. A mistyped `for="emailFeild"` is **silent**: it renders, it validates,
and clicking the words focuses nothing at all. And an id you write yourself has to be unique across a page you cannot
see all of at once — which is a promise no component can keep about itself.

⚑ **`labelFor:` and `labelledBy:` are not two spellings of one thing.** `aria-labelledby` gives the accessible NAME.
`<label for>` gives the name **and the click**: pressing the words focuses the input, which on a form of small
controls is most of the hit area, and which people use without thinking about it. Reach for `labelFor:` whenever the
target is a real form control; `labelledBy:` is for the rest — a `Box` acting as a dialog, named by its heading.

An element carrying `labelFor:` **is** a `<label>`, whatever it would otherwise render as. `for` on a `<span>` would
render, validate, and focus nothing.

⭐ **Inside a `foreach`, or a per-item block, each row gets its OWN pair.** A handle is scoped to one *iteration*, so
writing the pair straight into the loop is right — and it is the best answer there, because the name is already on
screen and does not have to be printed twice:

```osy title="inside a loop, each row gets its own handle pair" syntax
foreach (var m in members) {
  Row { Text(m.Name, labelFor: score); Input(value: m.Score, id: score); }
}
```

The two ends must be in the **same** row, though, and that is checked. A pair split across the loop boundary —
`labelFor:` outside it and `id:` inside, or the reverse — names one row's element from a place where there is no one
row, so the compiler refuses it and says which end to move.

⭐ **A wrapper names the control its caller hands it — `Slot(id: field)`.** This is what a form component *is*, and
until it existed a `Field` could not name its own field: the caption lives in the wrapper's tree and the input arrives
from the caller's, and a handle does not cross that boundary on its own.

```osy title="a wrapper naming the control its caller hands it" syntax
[Composable] component Field(string label) {
  render { Stack(gap: 1) { Text(label, labelFor: field); Slot(id: field); } }
}
Field("Name") { Input(value: draft.Name); }     // ← the caller writes nothing
```

The caption is written once and the pair is a real `<label for>`. `label: "Name"` on the `Input` is the fallback where
there is no caption to point at; here it would write the same words twice and give up the click. See [Slot (child content)](https://osysharp.com/reference/ui/slots/).

⚠ **A reference whose target did not render points at nothing**, and contributes nothing — which is right when there
is nothing to say. A `describedBy:` naming a message inside an `if` that did not run simply adds no description. What
cannot happen is a reference to a handle that does not exist at all: that is the compile error above.

### `osy lint` finds the ones you missed   {#lint}

Two rules, over every component you write:

- **`ui-control-state-not-announced`** — this component has a `checked`/`selected`/`open`/`sorted` parameter or field
  and sets no semantic prop anywhere, so it draws a state it never says.
- **`ui-control-without-an-accessible-name`** — this control's face is a glyph or a caller-filled slot and it takes no
  name.

```bash
osy lint
```

Both are `CONSIDER` rather than errors: a `bool open` might genuinely drive nothing a reader needs. In practice they
are nearly always real. Run over the platform's own kit when the rules were written, they returned ten controls —
every one of which is fixed, and the kit is now held to the same rule it ships you.

## Examples       {#examples}

A tab strip that says what it is. `Tabs` is the container, `Tab` carries its own selected state:

```osy title="a tab strip" test app=ui-accessibility
[Composable] component Tabs() {
  render { Row(role: UiRole.TabList, align: Align.End, gap: 1) { Slot; } }
}

[Composable] component Tab(string label, bool selected = false, Action onPress = null) {
  render { Pressable(label, role: UiRole.Tab, selected: selected, onClick: onPress); }
}
```

An icon-only button that its caller can name — the parameter is the whole point:

```osy title="carry what you know, take what you cannot" test app=ui-accessibility
[Composable] component IconAction(Action onPress, string label = "") {
  render { Pressable(onClick: onPress, label: label) { Slot; } }
}
```

A page shell with landmarks, so a reader can jump straight past the chrome instead of reading it every time:

```osy title="landmarks on a shell" test app=ui-accessibility
[Page("/shell")]
[AllowAnonymous]
component Shell() {
  render {
    Stack {
      Row(role: UiRole.Banner) { Text("Acme"); }
      Row {
        Stack(role: UiRole.Navigation, label: "Sections") { Text("Orders"); }
        Stack(role: UiRole.Main) { Text("…"); }
      }
      Row(role: UiRole.ContentInfo) { Text("(c) Acme"); }
    }
  }
}
```

A sortable column header. The arrow is decoration, because `sort:` has already said which way it goes:

```osy title="a sortable header" test app=ui-accessibility
[Composable] component SortHeader(string label, bool sorted = false, bool desc = false, Action onSort = null) {
  render {
    Row(role: UiRole.ColumnHeader, sort: sorted ? (desc ? UiSort.Descending : UiSort.Ascending) : UiSort.None,
        align: Align.Center, gap: 1) {
      Pressable(onClick: onSort) { Text(label); }
      if (sorted) { Text(desc ? "v" : "^", decorative: true); }
    }
  }
}
```

A checkbox that reports its state — the shape the platform's own `Checkbox` uses:

```osy title="a checkbox anyone can read" test app=ui-accessibility
[Composable] component Tickbox(bool checked, string label, Action onToggle = null) {
  render {
    Pressable(onClick: onToggle, role: UiRole.Checkbox, checked: checked, label: label) {
      Row(align: Align.Center, gap: 2) {
        Box(w: "18px", h: "18px", borderW: 1);
        Text(label);
      }
    }
  }
}
```

A field whose label points at its input and whose message describes it — the shape every form is made of:

```osy title="a field that is wired together" test app=ui-accessibility
[Composable] component LabelledField(string label, Binding<string> value, string error = "") {
  render {
    Stack(gap: 1) {
      Text(label, labelFor: box);
      Input(value: value, id: box, describedBy: note, invalid: error != "");
      if (error != "") { Text(error, id: note); }
    }
  }
}
```

Clicking the words focuses the input, and a screen reader reads the field and then the reason it was rejected, as one
thing. Neither is expressible with `label:` alone.

## See also       {#see-also}
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — the controls the platform ships, and the accessibility parameters each one takes
- [keys](https://osysharp.com/reference/ui/keys/) — making a custom surface keyboard-operable at all
- [Validation](https://osysharp.com/reference/ui/validation/) — where `invalid:` comes from on a field
- [Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/) — a UI test is the first non-visual consumer of your controls, and finds the same gaps
- [component](https://osysharp.com/reference/ui/component/) — declaring a component and its parameters


---

<!-- https://osysharp.com/reference/ui/animation/ -->

# animation — looping motion with no destination state

> An `animation` block declares reusable, looping motion — a shimmer, a pulse, an indeterminate progress hint. Its keyframe stops are written with the ordinary style props, so they are checked at compile time and can read theme tokens. The timing lives on the declaration, so a use site is a single `animation: Name` reference.

<!-- id: ui-animation · area: ui · stability: stable · html: https://osysharp.com/reference/ui/animation/ -->

## Summary        {#summary}
An **`animation`** declares motion that **loops with no destination state** — a skeleton **shimmer**, a **pulse**
on a live indicator, an **indeterminate progress** hint while a long job runs, or a highlight that **decays back**
after a row changes.

That is the gap it fills. A **transition** already covers every A→B state change: hover a button and its tint
moves, toggle a drawer and it slides. But some motion has no "to" value — a reconnecting spinner runs until the
connection comes back, and a "this row just changed" flash has to fade away by itself. Those are **events**, not
states, so there is nothing for a transition to move *toward*.

```osy title="a skeleton shimmer" test app=ui-animation
animation Shimmer {
  Duration = "1.5s";
  Easing = EaseInOut;
  Repeat = Infinite;
  0%   { Opacity = 0.4; }
  100% { Opacity = 1; }
}
```

A keyframe stop's body is **the ordinary style-prop vocabulary** — the same names a `variants` block uses. So a
stop is checked at compile time, a misspelled prop is an error rather than a silently dead line, and a stop can
read a theme token like any other style.

## Signature      {#signature}
```osy syntax
animation <Name> {
  Duration = "<time>";                 // optional — "1.5s" or "600ms"
  Easing   = <Linear|Ease|EaseIn|EaseOut|EaseInOut>;   // optional
  Repeat   = <Infinite|<count>>;       // optional — Infinite, or a whole number

  <percent>% { <StyleProp> = <value>; … }    // one block per stop
  from { … }                                 // an alias for 0%
  to   { … }                                 // an alias for 100%
}
```

## Description    {#description}

### Where an `animation` is declared — top level, not in a theme   {#placement}
An `animation` is **app-global**, declared at the top level beside `entity`, `enum` and `component`. It is not part
of a `theme`: a theme holds **tokens**, which are single values, while an animation is a **rule** with structure of
its own. And it is not declared inside a component, because motion like a shimmer is reused across many screens —
declaring it once is the point.

### Where do `Duration`, `Easing` and `Repeat` go?   {#timing}
`Duration`, `Easing` and `Repeat` are written **once**, where the animation is defined — not at every place that
uses it. A use site is therefore a single reference, and reading it tells you the whole story:

```osy title="one reference — the timing comes with it" test app=ui-animation
component Skeleton() {
  variants { base { Bg = Colors.Surface; Animation = Shimmer; } }
  render { Box(h: "16px"); }
}
```

All three settings are optional; anything you omit takes the platform's default (run once, at an even pace).

**`Easing`** is a fixed set of words: `Linear`, `Ease`, `EaseIn`, `EaseOut`, `EaseInOut`. A typo is a compile error
that lists the accepted words.

**`Repeat`** takes `Infinite` — the usual choice, because an animation exists for motion that runs until the work
ends — or a whole number for a fixed number of passes (`Repeat = 3`).

**`Duration`** is written as a time **string**: `"1.5s"` or `"600ms"`. A bare number is refused, because it would
be ambiguous between seconds and milliseconds.

### Describing the keyframes — `stop` blocks   {#stops}
Each stop says **where in the run** it applies and **what is true there**. Write them in any order — they run from
0% to 100% regardless:

```osy title="a three-stop pulse; declared out of order on purpose" test app=ui-animation
animation Pulse {
  Duration = "1.4s";
  Easing = EaseInOut;
  Repeat = Infinite;
  50%  { Opacity = 0.45; }
  0%   { Opacity = 1; }
  100% { Opacity = 1; }
}
```

`from` and `to` are accepted as aliases for `0%` and `100%`, so a keyframe set copied out of a stylesheet reads
the same here:

```osy title="from / to" test app=ui-animation
animation SlideIn {
  Duration = "200ms";
  Easing = EaseOut;
  from { TranslateX = "-100%"; }
  to   { TranslateX = "0"; }
}
```

A stop holds **style props only** — a flat list. Interaction states (`Hover`, `Focus`) and responsive overrides
belong in a component's `variants` block; they have no meaning partway through an animation.

### Using theme tokens in a stop    {#tokens}
Because stops use the ordinary style props, they can reference **theme tokens**. Motion then re-themes with
everything else — switching theme or color mode changes the animation with no code change:

```osy title="a stop reading theme tokens" test app=ui-animation
theme T { Colors { Surface = "#FFFFFF"; Accent = "#0077B6"; } }

animation Flash {
  Duration = "900ms";
  Easing = EaseOut;
  0%   { Bg = Colors.Accent; }
  100% { Bg = Colors.Surface; }
}
```

### Putting an animation on an element — the `animation` prop   {#applying}
Reference the animation by name with the `animation` style prop — either as an inline argument or in a `variants`
block:

```osy title="inline, on any element" test app=ui-animation
component Status() {
  render { Text("Reconnecting…", animation: Pulse); }
}
```

The name is **checked when you compile**. Referring to an animation that does not exist is an error naming the
ones that do — because the alternative failure has no symptom at all: the element renders perfectly and simply
never moves.

### Staggering: `animationDelay`    {#delay}
Two elements running one animation are in **lockstep** — which is right for a pair of skeleton rows and wrong for a
**chase**: a row of bulbs lighting in sequence, a wave across a bar chart, a spinner made of dots. That is one
animation with a per-element **offset**, and the offset is the `animationDelay` style prop:

```osy title="one animation, N elements, a chase" test app=ui-animation
animation Bulb {
  Duration = "1.2s";
  Easing = EaseInOut;
  Repeat = Infinite;
  0%   { Opacity = 0.25; }
  50%  { Opacity = 1; }
  100% { Opacity = 0.25; }
}

component Marquee() {
  render {
    Row(gap: 2) {
      foreach (var i in Enumerable.Range(0, 6)) {
        Box(w: 12, h: 12, rounded: 99, bg: "#F5C518", animation: Bulb, animationDelay: (i * 150) + "ms");
      }
    }
  }
}
```

Six bulbs, one declaration. Without the delay this is six `animation` declarations that differ only in where their
keyframes sit — which is what it used to cost.

The value is a **time string with its unit**: `"200ms"` or `"0.2s"`. A bare number is refused for the same reason
`Duration` refuses one — it is ambiguous between seconds and milliseconds, and CSS reads an unsuffixed number as
neither, so the declaration would be silently dropped. Any expression in scope may build it (`(i * 150) + "ms"`
above), so the offset can come from a loop index, a parameter or state.

A **negative** delay starts the animation already partway through, which is how you get a chase that is fully
running on the first frame rather than filling in over the first cycle: `animationDelay: (i * -150) + "ms"`.

### When *not* to reach for one    {#when-not}
If the motion has a **destination** — a color that settles, a panel that finishes opening, a button that grows on
hover — use a **transition** instead. It is simpler, it interrupts and reverses cleanly when the state changes
again, and it is what the state-change case is for. Reach for an `animation` when there is nothing to settle on.

## See also   {#see-also}
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the design tokens a keyframe stop can read.
- [style props](https://osysharp.com/reference/ui/styling/) — the style-prop vocabulary a stop's body is written in.
- [component](https://osysharp.com/reference/ui/component/) — `variants`, and where a use site lives.


---

<!-- https://osysharp.com/reference/ui/capture/ -->

# camera and microphone

> Take a photograph, record a video, or record audio. `Camera.Start()` asks for the device and `Camera()` shows the viewfinder; `Camera.Capture()` answers an `UploadedFile` — the same value the `Upload` control hands over, so a photograph is stored exactly like a chosen file. Every refusal is one catchable `DeviceUnavailableException`.

<!-- id: ui-capture · area: ui · stability: preview · html: https://osysharp.com/reference/ui/capture/ -->

## Summary        {#summary}
Two devices, one shape. `Camera` and `Mic` each have a **device** pair and a **recording** pair:

| | acquire / release | record |
|---|---|---|
| **camera** | `Camera.Start(facing: Back)` · `Camera.Stop()` | `Camera.Record()` · `Camera.StopRecording()` |
| **microphone** | `Mic.Start()` · `Mic.Stop()` | `Mic.Record()` · `Mic.StopRecording()` |

The camera also takes still photographs with `Camera.Capture()`, and shows what it sees through the `Camera()`
atom — the viewfinder.

Everything that produces a file — `Capture`, and both `StopRecording`s — answers an **`UploadedFile`**, which is the
same value the [upload](https://osysharp.com/reference/ui/upload/) control hands an `onUploaded` action. So a photograph and a chosen file are stored the
same way, by the same code.

## Signature      {#signature}
```osy syntax
Camera.Start(facing: CameraFacing.Back)        // ask for the camera; CameraFacing is Any (default), Front or Back
Camera.Start(withSound: true)                  // …and the microphone, if this app will record video WITH sound
Camera.Stop()                                  // release it

UploadedFile photo = Camera.Capture()          // photograph the viewfinder
Camera.Record()                                // start recording
UploadedFile clip = Camera.StopRecording()     // stop, and answer the whole clip

Mic.Start()                                    // ask for the microphone
Mic.Record()
UploadedFile note = Mic.StopRecording()
Mic.Stop()
```

## Description    {#description}

### A whole photo booth   {#example}
```osy title="ask, look, photograph, store" test app=file-manager
using Osysharp.Storage;

[Page("/booth")] [AllowAnonymous]
component Booth() {
  bool live = false;
  string problem = "";
  string shot = "";

  action Begin() {
    try {
      Camera.Start(facing: CameraFacing.Front);
      live = true;
      problem = "";
    } catch (DeviceUnavailableException ex) {
      problem = ex.Message;
    }
  }

  action Snap() {
    try {
      UploadedFile photo = Camera.Capture();
      shot = photo.Path;
    } catch (DeviceUnavailableException ex) {
      problem = ex.Message;
    }
  }

  action Done() { Camera.Stop(); live = false; }

  render {
    Stack(gap: 3) {
      if (!live) { Pressable("Turn the camera on", onClick: Begin); }
      if (live) {
        Camera();
        Row(gap: 2) {
          Pressable("Take a photo", onClick: Snap);
          Pressable("Done", onClick: Done);
        }
      }
      if (shot != "") { Image(shot); }
      if (problem != "") { Text(problem); }
    }
  }
}
```

### Being refused is normal, and it is one exception   {#refusal}
A person saying no to a permission prompt is the system working, not a failure. So every capture verb refuses the
same way — `DeviceUnavailableException` — and an app that uses both devices writes **one** `catch`.

`ex.Message` is a sentence you can show, and it says which of four things happened:

| what happened | what the person can do |
|---|---|
| they refused the prompt | change it from the padlock in the address bar |
| the machine has no camera or microphone | nothing — hide the feature |
| another application is holding the device | close the other application |
| **the page is not on https or localhost** | nothing they can do — see below |

⛔ **The last one is a deployment problem, not an answer.** A browser hands out a camera or microphone **only** on
`https://` or `localhost`, and there is no fallback of any kind — unlike [Clipboard](https://osysharp.com/reference/ui/clipboard/), which has a second path
for exactly this case. An app served over plain `http://` on a LAN address **cannot** have a camera. Nothing at
compile time can see that, which is why it arrives as this exception rather than as a refusal to build.

### The device and the recording are separate   {#two-pairs}
`Start`/`Stop` hold the **device**. `Record`/`StopRecording` are one **clip**. They are separate because holding the
microphone across several recordings is the normal case, and re-acquiring it per clip would prompt every time.

```osy syntax
Mic.Start();                          // one prompt
Mic.Record();  UploadedFile a = Mic.StopRecording();
Mic.Record();  UploadedFile b = Mic.StopRecording();
Mic.Stop();                           // the indicator goes out here
```

`Stop()` always means the device and `StopRecording()` always means the clip, on **both** ambients — even though
`Mic.Stop()` could have meant "stop recording", since a microphone has nothing to show. One vocabulary is worth more
than a locally shorter name.

### Sound is asked for when you start, not when you record   {#with-sound}
`Camera.Start()` acquires video only. To record video **with** sound, say so up front:

```osy syntax
Camera.Start(facing: Back, withSound: true);
```

Acquiring the microphone later, at `Camera.Record()`, would show a second permission prompt at the moment somebody
presses record — the worst possible time. And an app that only takes photographs should not be asking for a
microphone at all. One prompt, for exactly what you declared.

### The viewfinder   {#viewfinder}
`Camera()` is a live `<video>`. It takes no props of its own — which camera, and when, is `Camera.Start`'s business,
because that is the call that can be refused. Style it like any other element.

A component's verbs act on **its own** `Camera()`, the same rule [Canvas](https://osysharp.com/reference/ui/canvas/) follows: a viewfinder inside a nested
component belongs to that component, and the one that starts the camera must be the one that declares it.

### Playing it back   {#playback}
`Video` and `Audio` play a file the app has stored — a recording you just made, or anything else in its file store.
Both always show controls.

```osy syntax
Video(clip.Path);
Video(clip.Path, poster: still.Path, loop: true);
Audio(note.Path);
```

### What comes out   {#formats}
A photograph is a **JPEG** at the camera's own resolution — not the size of the element on screen, so a photo does
not change with the page layout.

A recording is whatever the browser records: **WebM** on Chrome and Firefox, **MP4** on Safari. There is no single
format every browser produces, so the platform asks for the best each one supports rather than insisting on one and
failing on the others. Whatever arrives, the server derives the stored file's real type from its bytes.

### Trying it before it exists   {#testing}
`Camera.Capture()` refuses if the camera has not produced its first frame yet, rather than storing a blank image. If
you photograph immediately after `Camera.Start()`, expect that refusal — its message says the camera is still
starting up.

## See also   {#see-also}
- [upload](https://osysharp.com/reference/ui/upload/) — the `Upload` control, which hands over the same `UploadedFile`
- [sound](https://osysharp.com/reference/ui/sound/) — playing audio the app ships, which is a different thing from recording it
- [Canvas](https://osysharp.com/reference/ui/canvas/) — the drawing surface, and the "a component acts on its own element" rule
- [component](https://osysharp.com/reference/ui/component/) — actions, state, and where these verbs are called from


---

<!-- https://osysharp.com/reference/ui/policy-controls/ -->

# canPress / canEdit / canSee

> `canPress`/`canEdit`/`canSee: <policy>` ties a control to a declared `policy`: `canPress` disables a button, `canEdit` makes a value control read-only, and `canSee` hides an element entirely — each unless the signed-in caller satisfies the policy. You declare the rule ONCE — on the entity or as a `policy` the server enforces — and the control simply REFLECTS it, live and reactively. It is a UX affordance, never the gate: the server still enforces the policy on every action, so a control that reflects the wrong answer can still never let a caller do something they may not.

<!-- id: ui-policy-controls · area: ui · stability: stable · html: https://osysharp.com/reference/ui/policy-controls/ -->

## Summary        {#summary}
`canPress: <policy>` ties a control's **pressable state** to a declared **`policy`**. The button is enabled when the
signed-in caller satisfies the policy and disabled when they don't — computed on the client, reactively, so it
reflects the current caller with no extra code.

```osy title="canPress mirrors the entity rule on the button" syntax
// Creating an organization is admins-only — declared once on the entity:
//   entity Organization { ... security { allow create when IsPlatformAdmin; } }
// The button that starts that create REFLECTS the same rule, so a non-admin sees it disabled:
Pressable(onClick: NewOrg, canPress: IsPlatformAdmin) {
  BtnPrimary("New organization") { Icon(Icons.Plus, size: 18); }
}
```

This is a **reflection, not a gate**. The server enforces the policy on the action itself; `canPress` only spares the
caller a click that would be refused. Because the rule lives in one place — the `policy` (see [page authorization (policies)](https://osysharp.com/reference/ui/authorize/)) — the
button can never drift out of sync with what the server actually allows.

A value control uses **`canEdit`** the same way — the field stays visible but becomes read-only unless the caller
satisfies the policy:

```osy title="the three policy props, on a real page" test app=ui-policy-controls
[Principal] entity User { [Required] string Email; }
[Role] enum AppRole { PlatformAdmin, Member }

entity RoleGrant {
  [Required] User User;
  [Required] AppRole Role;
  security { allow read when IsAuthenticated; }
}

policy IsPlatformAdmin => RoleGrant.Any(r => r.User == user && r.Role == AppRole.PlatformAdmin);

entity Application {
  [Required] string Slug;
  [Required] string Name;
  security { allow create, read, update when IsAuthenticated; }
}

[Page("/settings/{id}")] [Render(CSR)]
component OrgSettings(Guid id) {
  var org = Application.Single(a => a.Id == id);
  action Seed() { }
  action NewOrg() { }
  render {
    // The field shows the value to everyone, editable to admins:
    Input(value: org.Slug, canEdit: IsPlatformAdmin);

    // An admin-only affordance — a member never sees it (nothing is rendered, not just hidden):
    Stack(canSee: IsPlatformAdmin) {
      Button("Seed sample data", onPress: Seed);
    }

    // Disabled rather than hidden, and it says WHY.
    Pressable(onClick: NewOrg,
              canPress: IsPlatformAdmin,
              whenDenied: "Only platform admins can create organizations.") {
      Text("New organization");
    }
  }
}
```

To hide an element (and its whole subtree) unless the policy holds, use **`canSee`** on any element — a container is the
usual choice, so a whole section appears only for callers who satisfy the policy:

```osy title="canSee removes the subtree instead of disabling it" syntax
// An admin-only affordance — a member never sees it (nothing is rendered, not just hidden):
Stack(canSee: IsPlatformAdmin) {
  Button("Seed sample data", onPress: Seed);
}
```

## Signature      {#signature}
```osy syntax
<PressableControl>(canPress: <PolicyName>)   // → disabled unless the policy holds
<ValueControl>(canEdit: <PolicyName>)        // → read-only unless the policy holds
<AnyElement>(canSee: <PolicyName>)           // → not rendered at all unless the policy holds
```
- **`<PolicyName>`** — a declared `policy`, the same vocabulary [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) uses. A name that isn't a declared
  policy is a compile error (with a did-you-mean).
- **`canPress`** applies to **pressable** controls (a `Pressable` / button); **`canEdit`** applies to **value**
  controls (an `Input`) — used on a control that doesn't take the matching state, it is a compile error. **`canSee`**
  applies to **any element**; when the policy is false the element and its children render nothing at all.

## Description    {#description}
A `policy` names a condition about the caller — for example "is a platform admin", or "is an owner or admin of an
organization". You already write policies to gate pages ([page authorization (policies)](https://osysharp.com/reference/ui/authorize/)) and entity access. `canPress` lets the same
policy drive a control's enabled state, so the UI and the enforced rule are one declaration.

The control is **disabled until the policy is known to be true**. This is deliberate and fail-safe: while the data a
policy depends on is still loading, the control stays locked, then unlocks the moment the policy resolves true — it
never flashes enabled first. If the policy is false, the control stays disabled.

A policy usable on a control must be **self-scoped** — a check about the caller's own membership or roles, such as:

```osy syntax
policy IsPlatformAdmin => PlatformRoleGrant.Any(g => g.User == user && g.Role == PlatformRole.Admin);
policy IsOrgAdmin      => OrganizationMember.Any(m => m.User == user && m.Role != OrgRole.Member);
```

A policy that compares to data the control doesn't hold (a specific page's row, or an unrelated entity) can't be
reflected on a control today and is a compile error — keeping the clean syntax honest about what it can and can't show.

Only the caller's **own** rows are ever loaded to evaluate the reflection; nothing about other users is read to decide
whether your button is enabled.

## Saying WHY a control is locked — `whenDenied`   {#denial-hint}
Pair a policy prop with **`whenDenied`** to explain *why* a control is unavailable — the sentence shows as a tooltip
while the control is locked (disabled or read-only), and is silent once the caller satisfies the policy:

```osy syntax
Pressable(onClick: NewOrg,
          canPress: IsPlatformAdmin,
          whenDenied: "Only platform admins can create organizations.") {
  BtnPrimary("New organization") { Icon(Icons.Plus, size: 18); }
}
```

The message lives at the **control**, not the policy — the same policy can deny different actions for different
reasons. It renders to the native `title` attribute only while the control is locked, so a normal reader hovers to see
why an action is off, and assistive technology reads it either way.

## Policy as a value {#policy-value}
Sometimes you don't want to reflect a policy onto one control — you want to **branch the whole layout** on it, or reuse
the answer in several places. A policy can be used directly **as a `bool` value**: in an `if`, or held in a `live var`.

```osy title="branching the whole layout on a policy" test app=ui-policy-controls
policy CanManageApp(Application a) => RoleGrant.Any(r => r.User == user && r.Role == AppRole.PlatformAdmin);

[Composable] component AppRosterEditor(Application app) { render { Text(app.Name); } }
[Composable] component AppRosterReadonly(Application app) { render { Text(app.Name); } }

// A page that lays out differently for someone who can manage the app vs. a plain viewer:
component AppDetail(string slug) {
  var app = Application.Single(a => a.Slug == slug);
  render {
    if (CanManageApp(app)) {
      // the manager's view — an editable roster, an invite button …
      AppRosterEditor(app: app);
    } else {
      // the read-only view
      AppRosterReadonly(app: app);
    }
  }
}
```

Hold the answer once and reuse it with a **`live var`** — it tracks reactively, exactly like a control prop:

```osy title="holding the answer in a live var" test app=ui-policy-controls
[Composable] component NavLinks() { render { Text("nav"); } }

[Page("/toolbar")] [Render(CSR)]
component Toolbar() {
  live var canManage = IsPlatformAdmin;   // a bool, reactive to the caller's own rows
  action OpenSettings() { }
  render {
    if (canManage) { Button("Settings", onPress: OpenSettings); }
    NavLinks();
  }
}
```

Both forms lower to the **same** self-list reflection a `canPress:` prop uses — client-computed, reactive, and reading
only the caller's own rows. And they carry the same guarantee: this is **render-only**. Branching the layout hides an
affordance a caller can't use; it does not protect anything. The server still enforces the policy on every read and
every action, so a page that showed the manager's view to the wrong caller would still refuse every mutation behind it.

The rules match a control prop: the value must be a **declared, self-scoped** `policy` (a typo is an unknown-identifier
error, not a silently-false branch), and a parameterized policy is applied to a value the page already holds
(`CanManageApp(app)`). A policy is **not** callable in a server position — you can't put one in an entity query's
`where`; it is the server's own enforcement, never a query term.

## See also   {#see-also}
- [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) — gate a whole page or route on a policy (the enforced side).
- [component](https://osysharp.com/reference/ui/component/) — where controls and their handlers live.
- [style props](https://osysharp.com/reference/ui/styling/) — the visual vocabulary a control's variants assign.


---

<!-- https://osysharp.com/reference/ui/control-chunks/ -->

# chunks — assets a control loads on demand

> A `chunks { }` block declares assets a control ships but does not need at mount — a maths renderer, a diagram engine, a stylesheet only one feature uses. The shim asks for one by name when it needs it, so a heavy optional feature costs nothing on the pages that never use it.

<!-- id: ui-control-chunks · area: ui · stability: stable · html: https://osysharp.com/reference/ui/control-chunks/ -->

## Summary        {#summary}
A control is one bundle, and every page that mounts it downloads all of it. That is the right shape for a control's own
code, and the wrong shape for a feature most documents never touch: a maths renderer is a quarter of a megabyte, a
diagram engine several times that, and bundling either means charging every reader for a thing one page in fifty uses.

A **`chunks { }`** block is the second address. It names the assets a control ships separately, and the shim asks for
one **by name** at the moment it needs it:

```ts syntax
if (documentHasMath) {
  const { render } = await import(host.chunkUrl("Math"));
  render(el, source);
}
```

A chunk is stored, content-addressed, cached and garbage-collected exactly like the bundle beside it. What is different
is only that the platform does not load it — **the control decides when, and whether at all**.

A chunk can be a single file or a whole **package** directory. The package shape exists for libraries that load their
own parts at run time — a worker, a `.wasm` sibling, a renderer per diagram type — which no bundler can flatten into
one module. See **One file, or a package** below.

## Signature      {#signature}
```osy syntax
control <Name> {
  chunks {
    <ChunkName>;
    <ChunkName>;
  }
}
```

Registered per name — as one file, or as a directory:

```bash
osy control add controls/editor.js \
  --chunk Math=vendor/temml.js \
  --chunk MathCss=vendor/temml.css \
  --chunk-package Diagrams=dist/diagrams/index.js
```

## Description    {#description}

### Declaring a chunk and pointing it at a file   {#declaring}

The declaration names the chunks; the registration says which file each one is.

```osy title="an editor whose maths support is optional" test app=ui-control-chunks
control MarkdownEditor {
  contractVersion "1.1"
  participation headless
  props { string ownerType; }
  chunks {
    /// A MathML renderer, loaded the first time a document contains `$…$`.
    Math;
    /// Its stylesheet.
    MathCss;
  }
}

[Page("/doc")] [AllowAnonymous]
component DocPage() {
  render {
    MarkdownEditor(ownerType: "Document");
  }
}
```

The two halves are checked against each other. A `--chunk` name the control does not declare is refused, naming the
ones it does; a declared chunk with no file registered is a **compile error** once anything renders the control, and a
warning while nothing does — so the bundle can be registered first and its chunks next, without a broken state in
between.

Nothing about a chunk reaches the app. A `chunks { }` block changes no call site, no prop, and no page: it is a
statement about how the control ships itself.

### Getting a chunk's URL in the shim — `host.chunkUrl`   {#asking}

`host.chunkUrl(name)` returns a same-origin URL. The generated typings key it to the names you declared, so a typo does
not compile:

```ts syntax
export interface MarkdownEditorHost {
  chunkUrl(name: "Math" | "MathCss"): string;
}
```

**It returns a URL, not a module**, and that is deliberate. One member then covers both kinds of asset — a module you
`import()`, and a stylesheet you put in a `<link>`:

```ts syntax
// a module
const temml = await import(host.chunkUrl("Math"));

// a stylesheet
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = host.chunkUrl("MathCss");
document.head.appendChild(link);
```

A member that returned a *module* could not express the stylesheet at all, and one that decided *when* to load would be
answering a question only the control knows the answer to.

The URL is **content-addressed and immutable**, so caching it forever is safe and re-asking is free — `chunkUrl` does no
work beyond a lookup. Loading twice is the shim's own concern: keep the promise.

### One file, or a package    {#one-file-or-package}

A chunk name resolves to one of two things, and the registration decides which. The declaration is the same either
way — a chunk name is a name, and `host.chunkUrl()` returns something you can load.

**A single file** is served at its own content address, with **no directory around it**. So a relative specifier
inside one has nothing to resolve against:

```ts syntax
import { helper } from "./helper.js";   // ✗ refused in a single-file chunk — there is no "." to be relative to
```

`osy control add` refuses that at registration and names the specifier it found. Bundle the chunk to one file
(`esbuild --bundle --format=esm`), or register it as a package instead.

The same applies to a stylesheet's `url()` in a single-file chunk. Its CSS cannot reference a font by relative path;
register the face with [web fonts — shipping a typeface with your app](https://osysharp.com/reference/ui/web-fonts/) and point the `url()` at the address that returns.

**A package** is a whole directory, served under a real base — so relative imports resolve exactly as they do on a
static host:

```bash
osy control add controls/editor.js --chunk-package Diagrams=dist/diagrams/index.js
```

You give the **entry file**; the package is the directory containing it, taken recursively. `host.chunkUrl("Diagrams")`
returns that entry's URL, and everything the entry reaches from there — `./chunks/x.js`, a `.wasm` sibling, a worker
started with `new Worker(new URL("./worker.js", import.meta.url))` — resolves against the directory it genuinely sits
in.

Reach for a package when a library **loads its own parts at run time**. Many do, and flattening one into a single file
is both wasteful and sometimes impossible: a diagram engine that lazy-loads a renderer per diagram type becomes one
module carrying every renderer, and a library that starts a worker or fetches a `.wasm` cannot be bundled at all.

⚠️ **Register your own build output, never a package's distribution directory.** A published package typically ships
several build flavours and a full type-definition tree — one common diagram library's is 83 MB across 1167 files.
Point a bundler at the library and register what it writes:

```bash
esbuild node_modules/some-lib/dist/index.mjs --bundle --format=esm --minify --splitting --outdir=dist/diagrams
osy control add controls/editor.js --chunk-package Diagrams=dist/diagrams/index.js
```

A path inside `node_modules` is refused by name, before anything is read.

A package's identity is its **whole file set**, so re-registering an unchanged directory writes nothing, and a file
added or deleted since it was registered fails the next compile rather than shipping a set nobody registered.

### What may be served    {#content-types}

A chunk's content type comes from its extension, and the set is closed:

| Extension | Served as |
|---|---|
| `.js`, `.mjs` | `text/javascript` |
| `.css` | `text/css` |
| `.json` | `application/json` |
| `.wasm` | `application/wasm` |
| `.woff2`, `.woff`, `.ttf` | the matching font type |

Anything else is refused. `.html` and `.svg` are refused deliberately: both would give the app a same-origin address
that executes script, and no control needs one.

Anything else is refused, in a package member exactly as in a single file — and it is refused **by name** rather than
skipped, so a build output containing (say) a source map tells you which file to exclude instead of registering
cleanly and 404ing later on the one file nobody thought about.

The per-asset size limit is the bundle's — **5 MB**, applied per file — and every asset a control ships counts toward
the app's total.

### A chunk is not a prop, a style, or a slot    {#not-the-others}

Four ways a control takes something from outside, and they answer different questions:

| | What it is | Decided by |
|---|---|---|
| **prop** | a value the control renders with | the app, per call site |
| **style** | a look knob the app may override | the app, or its theme |
| **slot** | content the app writes inside the control | the app |
| **chunk** | code or an asset the control ships itself | the **control** |

A chunk is the only one of the four the app knows nothing about. If an app should be able to decide something, that is
a prop; if it should be able to supply something, that is a slot. A chunk is the control's own weight, moved off the
critical path.

Unlike `styles { }`, a chunk is legal at **every** participation rung — including `opaque`. A chunk is neither look nor
app data; it is the control's own code, and an opaque island is exactly the kind of control that ships a heavy renderer.

## Examples       {#examples}

A diagram control that loads its engine once, on the first diagram it meets:

```osy title="a chunk nothing pays for until a diagram appears" test app=ui-control-chunks-diagram
control DiagramView {
  contractVersion "1.1"
  participation opaque
  props { string source; }
  chunks { Engine; }
}
```

```ts syntax
let engine: Promise<{ render(el: HTMLElement, src: string): void }> | undefined;

export function mount(el, props, host) {
  const draw = async (src: string) => {
    // Loaded ONCE and remembered — a second diagram on the page costs nothing.
    engine ??= import(host.chunkUrl("Engine"));
    (await engine).render(el, src);
  };
  if (props.source) void draw(props.source);
  return {
    update(next) { if (next.source) void draw(next.source); },
    destroy() { el.replaceChildren(); },
  };
}
```

The page that renders `DiagramView` with no source downloads the control and nothing else.

## See also       {#see-also}
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the `control` block chunks are declared in
- [commands — the verbs a control accepts](https://osysharp.com/reference/ui/control-commands/) — the verbs a control publishes
- [styles — a control's own look knobs](https://osysharp.com/reference/ui/control-styles/) — a control's own look knobs
- [web fonts — shipping a typeface with your app](https://osysharp.com/reference/ui/web-fonts/) — how an app ships a typeface, including one a chunk's stylesheet names


---

<!-- https://osysharp.com/reference/ui/palette/ -->

# color palettes

> `Palette.From("#seed")` turns one brand color into a full ramp of shades. A bare reference (`Primary`) is the seed itself; `Primary.Hover` and `Primary[600]` reach the named and numbered steps of its ramp.

<!-- id: ui-palette · area: ui · stability: stable · html: https://osysharp.com/reference/ui/palette/ -->

## Summary        {#summary}
A **palette** turns a single brand color into a full **ramp** — a scale of shades from very light to very dark, plus
named steps for common jobs like a hover state. You give one seed color; the palette generates the rest, evenly, so a
button has a darker shade to hover to and a light tint to sit on without you hand-picking each one.

```osy title="one seed, a whole ramp" test app=ui-palette
theme Default {
  Colors {
    Primary = Palette.From("#0077B6");   // one seed → a whole ramp
  }
}
```

## Signature      {#signature}
```osy syntax
Primary = Palette.From("#0077B6");   // declare a palette from a seed color
Bg = Primary.Hover;                  // a named step
Bg = Primary[600];                   // a numbered step (50…950)
```

## Description    {#description}

### One seed, a full ramp   {#from}
`Palette.From("#seed")` declares a **palette token**. From the one seed color it generates an even ramp of shades —
computed in a perceptual color space, so the steps read as evenly spaced and the hue stays true from the lightest tint
to the darkest shade (a plain "lighten/darken" drifts and muddies; this does not).

```osy title="two ramps" test app=ui-palette-two
theme Brand {
  Colors {
    Primary = Palette.From("#0077B6");
    Accent  = Palette.From("#C0392B");
  }
}
```

The seed must be a hex color literal (`"#0077B6"` or the short `"#07B"`). Anything else — `Palette.From(Primary)`,
`Palette.From()` — is a compile error, so a mistyped palette can't slip through as an empty one.

### Getting the exact seed color back — the bare name   {#base}
Referencing the palette by its **bare name** gives you the **seed exactly as you typed it** — your brand color, not a
generated approximation of it:

```osy title="the ramp's base step" test app=ui-palette
[Composable] component Fill() {
  variants { base { Bg = Colors.Primary; } }   // Bg is exactly #0077B6
  render { Box(); }
}
```

Like any [theme tokens](https://osysharp.com/reference/ui/theming/) token reference, a bare palette name is a **living reference**, so restyling the seed carries
through everywhere it's used with no rebuild.

### Named steps   {#semantic-steps}
A palette exposes a small set of **named steps** for the jobs a color actually does in an interface. Reach them with a
member access:

```osy title="named steps" test app=ui-palette
[Composable] component Swatch() {
  variants {
    base {
      Bg = Colors.Primary; Color = Primary.OnColor;   // fill + a legible foreground on it
      Hover { Bg = Primary.Hover; }            // a step darker on hover
    }
  }
  render { Box(); }
}
```

| Step | What it's for |
|---|---|
| `Subtle` | a faint tint — a hover background, a selected row |
| `Muted` | a soft fill — a chip, a well |
| `Default` | the palette's mid shade |
| `Hover` | one step stronger than the base — a hover/emphasis fill |
| `Active` | stronger still — a pressed state |
| `Strong` | the darkest useful shade — high-emphasis text or borders |
| `OnColor` | the **foreground** color to place *on* the palette — chosen automatically (black or white) for legible contrast against the base |

`OnColor` is the one that isn't a shade of the hue: it's whichever of black or white reads clearly on your brand
color, worked out for you — so `Color = Primary.OnColor` is legible whether your brand is a deep navy or a pale amber.
Naming a step that doesn't exist (`Primary.Hund`) is a compile error listing the ones that do.

### Numbered steps   {#numeric-steps}
Under the named steps is a **numeric ramp** — `50` (lightest) through `950` (darkest), in the familiar `50, 100, 200 …
900, 950` scale. Use it when you want an exact step the names don't single out:

```osy title="numbered steps" test app=ui-palette-steps
theme Brand {
  Colors {
    Primary = Palette.From("#0077B6");
    Line    = Primary[200];    // a light hairline from the same ramp
    Ink     = Primary[900];    // near-black, still on-brand
  }
}
```

The named steps are aliases over this ramp (`Hover` is the `700` step, `Default` the `500`, and so on), so
`Primary.Hover` and `Primary[700]` are the same color — pick whichever reads better where you use it. A number outside
the `50…950` scale (`Primary[550]`) is a compile error listing the valid steps.

### Where palettes work   {#where}
A palette step is an ordinary style value, so it works anywhere a color does — a **theme token**:

```osy title="one token derived from another's step" test app=ui-palette-derive
theme Brand {
  Colors {
    Primary = Palette.From("#0077B6");
    Focus   = Primary.Active;    // derive one token from another palette's step
  }
}
```

…and a **`variants` style-prop** (as in the button above). In both, a palette step stays a **living reference** into the
ramp — change the seed and every shade derived from it moves with it, with no rebuild.

## See also   {#see-also}
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the `theme` block, token groups, references, and dark mode.
- [component](https://osysharp.com/reference/ui/component/) — declaring a component and styling it with `variants`.


---

<!-- https://osysharp.com/reference/ui/control-commands/ -->

# commands — the verbs a control accepts

> A `commands { }` block declares the verbs a control accepts — the mirror of its events. An event is the control telling the app something happened; a command is the app asking the control to do something. Declaring them is what lets the platform hold the shim to its own contract.

<!-- id: ui-control-commands · area: ui · stability: stable · html: https://osysharp.com/reference/ui/control-commands/ -->

## Summary        {#summary}
A control's contract has always run one way: **props in, events out**. An app can hand a control values and be told
when something happened, but it has no way to name something the control DOES.

A **`commands { }`** block is the other direction. It declares the verbs the control publishes — `SelectAll`,
`ExportCsv(string filename)` — as a typed, checked part of its contract rather than a method a caller has to know
about from documentation.

A control declares its verbs, the generated typings require the shim to implement them, and the platform refuses a
shim that does not. An app reaches them two ways: from content it writes inside one of the control's own
[chrome slots](#invoking), and by [handing a verb back as a value](#invoking-as-a-value) — an entry in a list the
control renders. Both name a verb; neither hands the app a callable.

The block is optional, and a control that declares none is unaffected.

## Signature      {#signature}
```osy syntax
control <Name> {
  commands {
    <CommandName>;
    <CommandName>(<Type> <param>, …);
  }
}
```

## Description    {#description}

### Declaring the verbs a control publishes   {#declaring}

```osy title="a grid that publishes two verbs" test app=ui-control-commands
control DataGrid {
  contractVersion "1.1"
  participation headless
  props { string title; }
  events { rowPicked(int index); }
  commands {
    /// Select every row.
    SelectAll;
    ExportCsv(string filename);
  }
}
```

Events and commands are the same idea pointing opposite ways, and the generated typings read that way — one typed
`emit` overload per event on the **host**, one typed method per command on the **handle**:

```ts
export interface DataGridHost {
  emit(event: "rowPicked", index: number): void;       // OUT — the platform provides it
}

export interface DataGridHandle extends ControlHandle<DataGridProps> {
  commands: {                                          // IN — the shim implements it
    selectAll(): void;
    exportCsv(filename: string): void;
  };
}
```

A command is **declared PascalCase** (a name written in Osy#) and **implemented camelCase** (a JavaScript method).
Both sides derive that mapping from the declared name, so they cannot drift.

A command and an event cannot share a name: one name would mean both "tell the app this happened" and "ask the
control to do this".

### The shim must implement them    {#implementing}

Commands land on what `mount` RETURNS, not on the host, because they are things the control provides:

```ts syntax
export function mount(el, props, host) {
  return {
    update(next) { … },
    destroy() { … },
    commands: {
      selectAll() { … },
      exportCsv(filename) { … },
    },
  };
}
```

**The platform checks them when it takes the handle** — at the mount that produced it, exactly as it checks `update`
and `destroy`. A verb a control publishes and never writes fails there, loudly, naming the missing method. The
alternative is that it fails the first time anyone asks for it, which is a user, in a browser, a long way from the
code at fault.

### Publishing a verb is a decision    {#what-to-publish}

A command is not a way for an app to reach into a control. It is a named, typed verb the control **chose** to make
part of its contract, so what an app can ask for is exactly what the control decided to publish — and it can be
versioned like anything else in the block.

Prefer a **prop** for state (`readOnly`, `density`) and a **command** for an action that has no resting value.
"Selected" is a state; "select everything, now" is not.

### Invoking a command — a chrome slot    {#invoking}

A control declares a **`slots { }`** block naming regions of its own interface that an app fills. The content of such
a slot is handed the control's **commands**, so the app writes the chrome and the control keeps the behaviour behind
it:

```osy title="the app writes the find bar; the editor keeps the finding" test app=ui-control-commands-findbar
control MarkdownEditor {
  contractVersion "1.1"
  participation headless
  props { string title; }
  commands { FindNext; FindPrev; ReplaceAll(string text); }
  slots { FindBar; }
}

[Page("/doc")] [AllowAnonymous]
component DocPage() {
  action Close() { }
  render {
    MarkdownEditor(title: "Doc") {
      slot FindBar { c =>
        Row {
          Button("Previous", onPress: c.FindPrev);
          Button("Next", onPress: c.FindNext);
          Button("Replace all", onPress: () => c.ReplaceAll("draft"));
          Button("Done", onPress: Close);
        }
      }
    }
  }
}
```

**Values ride as props, verbs as commands.** A search box has a resting value, so it is state the app owns and hands
in (`findQuery:`); "replace every match, now" has no resting value, so it is a command. That split is what lets a
whole find bar — boxes, count, buttons, and whether it is open at all — be the app's, while the matching and the
document edits stay the control's. The count comes back as an ordinary event.

`c` is the control's commands. Naming them is the point: the app's **own** actions stay reachable in the same content
(`onClick: Close`), and which of the two a name means is never in doubt. Had the commands simply been in scope
unqualified, one could shadow a page action — and the failure would be a button that works and does the wrong thing.

`c.FindNxt` is a compile error listing the commands the control does declare. A command **with parameters** cannot be
named bare — there would be nothing to supply them — so it is CALLED instead, in the form every event handler already
uses: `onClick: () => c.ReplaceAll("draft")`, as above. The arguments are evaluated where they are written, when the
content is built.

**Where it renders is the control's decision.** The shim asks for the slot and positions it; an app cannot put content
somewhere the control did not offer, and a shim that ignores a slot simply shows nothing.

**What an app never gets is the callable itself.** It names a verb; the platform is what turns that name into a call
on the control. So a slot's content can reach exactly the set of verbs the control published, and nothing else.

### Invoking a command — a verb as a VALUE    {#invoking-as-a-value}

A chrome slot covers app content that CALLS the control. The other direction is a verb the app hands **back** — an
entry in a list the control renders itself. That is what a menu wants: the control owns the menu's look and its
keyboard model, and the app supplies what should be in it.

A control declares its item shape as an ordinary `class`, with an **`Action`**-typed member for the verb:

```osy title="an editor whose block menu takes the app's own entries" test app=ui-control-commands-menu
class MenuEntry {
  public string Label;
  public Action Run;
}

control MarkdownEditor {
  contractVersion "1.1"
  participation headless
  props {
    string title;
    MenuEntry[] extraItems = [];
  }
  commands {
    DeleteBlock;
    TurnIntoHeading(int level);
  }
}

[Page("/doc")] [AllowAnonymous]
component DocPage() {
  action Archive() { }
  render {
    MarkdownEditor(title: "Doc", extraItems: [
      new MenuEntry { Label = "Archive", Run = Archive },
      new MenuEntry { Label = "Delete block", Run = MarkdownEditor.DeleteBlock },
      new MenuEntry { Label = "Make it a heading", Run = () => MarkdownEditor.TurnIntoHeading(2) }
    ]);
  }
}
```

**One member takes either kind of verb**, and that is the point. A real menu is a mix: some entries run the app's own
code, some run the control's. Split into separate props and every app has to reassemble one ordered list from two —
and the order between them is lost.

**`Action` is the type**, the same one a component parameter uses for a callback (`component X(Action onPress)`). It
is always nullary; arguments are bound at the call site.

The four spellings, and nothing else:

| Written | Runs |
|---|---|
| `Run = Archive` | an `action`/`method`/callback parameter of this component, taking no arguments |
| `Run = () => Archive(doc.Id)` | the same, with arguments — evaluated where they are written |
| `Run = MarkdownEditor.DeleteBlock` | a command of the control **being called** |
| `Run = () => MarkdownEditor.TurnIntoHeading(2)` | the same, with arguments |

The qualifier must be the control being called: naming another control's verb is a compile error, because this entry
is handed to *this* control and nothing else can run it. A verb that does not exist, a parameterised one named bare,
the wrong number of arguments, and a plain value where a verb belongs are each refused with the working spelling named.

An `Action` member is **only** settable in a control's props at a call site. It holds a verb, not a value: there is no
storage for it, no wire form to the server, and away from a call site nothing that could run it. For the same reason
it is legal only on a `class` — a persisted `entity` refuses it.

**The shim receives a plain function.** Its generated typings say `Run: () => void`, and that is the whole contract:
it cannot tell an app action from one of its own commands, which is exactly why one member takes both.

### Reaching a control from ANYWHERE else    {#invoking-elsewhere}

Not yet possible — both forms above put the app's name for a verb *inside* the control's own call. An app action
calling a mounted instance from somewhere else entirely is a separate design (it needs instance naming and a "not
mounted yet" answer), and it is deliberately unbuilt until something real cannot be expressed without it. The
shim-side contract does not change if it lands.

What NOT to do meanwhile is fake it with a prop that means "go" — an incrementing counter the control watches for
changes. It reads as state, it is not, and it breaks the moment two callers want the verb in one render.

## Examples       {#examples}

A media player, where the split between props and commands is the clearest:

```osy title="state as props, actions as commands" test app=ui-control-commands-player
control Player {
  contractVersion "1.1"
  participation opaque
  props {
    string src;
    bool loop = false;
  }
  events { ended(bool natural); }
  commands {
    Play;
    Pause;
    SeekTo(int seconds);
  }
}
```

`loop` is a resting value the app owns, so it is a prop. `Play` has no resting value — asking twice means asking
twice — so it is a command.

## See also       {#see-also}
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the `control` block these are declared in
- [styles — a control's own look knobs](https://osysharp.com/reference/ui/control-styles/) — the other opt-in rung: a control's own look knobs
- [probe — what a control says about itself](https://osysharp.com/reference/ui/control-probe/) — the other opt-in rung a shim implements on its handle: what the control says about itself
- [Slot (child content)](https://osysharp.com/reference/ui/slots/) — slots in general, including a component's own


---

<!-- https://osysharp.com/reference/ui/theme-override/ -->

# compiling with a different theme

> `--theme <file>` compiles an app with the theme in that file INSTEAD of its own. It is a replacement, never a merge, so the look you get is one you can point at a file for. Use it to present several apps consistently, or to ship one app under more than one brand.

<!-- id: ui-theme-override · area: ui · stability: stable · html: https://osysharp.com/reference/ui/theme-override/ -->

## Signature   {#signature}
```text
osy compile  --theme <file>
osy validate --theme <file>
```

## Summary        {#summary}
An app's look lives in its `theme` block ([theme tokens](https://osysharp.com/reference/ui/theming/)). **`--theme <file>` compiles the app with a different
one**, without editing a line of it:

```console
$ osy validate --theme ../brand/acme.osy     # offline, no database — check it resolves first
$ osy compile  --theme ../brand/acme.osy     # then compile the app wearing it
```

Two things this is for:

- **One brand, many apps.** A theme cannot be imported across apps — a manifest globs its own directory — so
  sharing a house style otherwise means copying a file into every project. One file passed at compile time
  dresses all of them.
- **Presenting several apps together.** Documentation and demos show an app to explain a *feature*. When the
  apps each have their own palette, a reader compares styling instead of reading the point. Compile them under
  one theme for the occasion; the apps keep their own.

## What it does   {#description}

`--theme` takes the `theme` block out of the app's own sources and puts the one in your file there instead. The
rest of the app — its entities, its pages, its components — compiles exactly as it always does; only the tokens
change. Nothing in the app has to be written differently to be themeable this way.

## It replaces, it does not merge   {#replaces}

Every `theme` declaration in the app's own sources is removed, and the file you pass supplies the theme. Tokens
the app declared and the override omits are **gone**, not inherited.

This is deliberate. A merge would produce a theme that exists in neither file — so the thing on screen could not
be reproduced by compiling anything, which is exactly what you need from a screenshot or a shipped build.

## A token the override lacks is an error, by name   {#missing-tokens}

Because it replaces, an app that names a token your file does not declare will not compile:

```text
model/chrome.osy:12:53  ERROR  RESOLVE_ERROR  UI: the `Colors` group declares no token 'Bulb'
  — its tokens are Bg, Border, Danger, Muted, OnBg, OnPrimary, Primary, Success, Surface, …
```

That is the useful answer, not an obstacle: it tells you either to add the token to your theme, or that this
app's look is not substitutable. An app built on a handful of semantic colours takes almost any theme; one whose
palette *is* its subject — a game, a visual demo — will list everything it needs, which is a fair description of
why it should keep its own.

**So a theme meant to dress several apps is a superset**: it declares every token those apps name. Start from the
kit's own token names ([theme tokens](https://osysharp.com/reference/ui/theming/)) — an app that shadows kit tokens rather than inventing parallel ones is an
app almost any override fits.

## The swap is announced   {#announced}

Every run says what it did:

```text
⚠ theme override: acme.osy replaces `Doc` in model/theme.osy.
```

The app is not wearing its own look, so the output says so — a build or a screenshot taken from it should never
be mistaken for the app as it ships.

## What goes in the file you pass   {#the-file}

One `theme` block, and normally nothing else:

```osy title="a theme file to pass to --theme" test app=ui-theme-override
theme Docs {
  Colors {
    Primary   = "#125E7A";
    OnPrimary = "#FFFFFF";
    Bg        = "#FAF9F7";
    OnBg      = "#14181D";
    Surface   = "#FFFFFF";
    Border    = "#E4E2DC";
  }
  Radius { Md = "10px"; }
}
```

A file declaring **no** theme is refused, and so is one declaring **two** — with two, which one dressed the app
would depend on declaration order, which is not something you should have to know to read a screenshot.

Anything else in the file compiles as usual, and a `theme` block in the app that shares a file with components
loses only the block: the rest of the file is untouched.

## See also   {#see-also}
- [theme tokens](https://osysharp.com/reference/ui/theming/) — declaring tokens, the groups they live in, and light/dark modes.
- [color palettes](https://osysharp.com/reference/ui/palette/) — turning one seed colour into a full ramp.
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — the starter theme every app inherits, and the token names to shadow.


---

<!-- https://osysharp.com/reference/ui/component/ -->

# component

> The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live` queries/computeds, actions, methods), and a declarative render tree. A page is a component bound to a route.

<!-- id: ui-component · area: ui · stability: stable · html: https://osysharp.com/reference/ui/component/ -->

## Summary        {#summary}
A `component` is the ONE archetype for all UI: a bounded reactive unit with typed props, reactive members, and a
declarative render tree. A page is a component bound to a route (`[Page]`); a list row, a dialog, and the app
shell are all components. The compiler lowers a component to typed metadata; the client runtime fetches that tree
and renders it — apps never ship JavaScript.

## Signature      {#signature}
```osy syntax
[Page("/route/{param}")]        // optional: bind to a route (route params bind to same-named props)
[Render(SSR|SSG|ISR|CSR)]       // delivery mode; unrouted components inherit their host's
[AllowAnonymous]                // opt this route out of auth (public); routes require auth BY DEFAULT
[Authorize(Policy)]             // optional: narrow a (default-)protected route to a named policy (checked reference)
component Name(Type prop, …) {
  Type name = init;                // component field — a client SNAPSHOT (assignable; frozen until reassigned)
  var name = Entity.ToList();      // a server read, taken ONCE (a snapshot; not kept in sync)
  live var name = Entity.ToList(); // a reactive query — kept current via the change channel
  live var name = expr;            // a tracked computed — client-side; may call and allocate, never the server
  action name(params) { … }        // event handler (client interpreter)
  RetType name(params) => expr;    // plain helper method (or { block })
  render { … }                     // the declarative render tree
}
```

## Description    {#description}

### Where does a component keep its state?   {#members}
A component is a class, and its **fields are its reactive instance state** — so there is no `state`/`query`/`derived`
keyword to declare one. A `Type name = init;` (or `var name = …;`) written **directly in the component body, above
`render`** is a reactive field — the same place a C# class puts its fields; whether it
holds a **client value** or a **server read** is inferred from the initializer, not spelled: an initializer that reads
an entity set (`Order.ToList()`, `User.Single(…)`) is a server read; anything else — a scalar, a `new Entity{}` ghost,
a bare field, or a query over a *local* collection — is a client value. (Method-locals inside an `action`/method stay
transient, as in any class.)

**One reactive marker: `live`.** A `live var` **tracks** — it stays in sync with what it reads:
- `live var xs = Entity.ToList();` is a reactive **query** (a data-change signal refetches it);
- `live var y = <expr>;` is a tracked **computed** (it recomputes when its inputs change).

A **naked `var` is a snapshot** — it reads its value once and freezes. It is a *cut*: reactivity does not flow through
it. `var rows = xs.ToList();` copies `xs`'s current rows and never updates again, even as `xs` does; a refresh is a new
assignment, not an implicit update. So use `live var` when a value should stay current, and a plain `var` when you want
a fixed copy.

⚑ **Holding WHICH ROW an editor or a dialog is for is a field like any other** — `Album? editing;` — and it is the
question people arrive at this section already asking. It has its own answer below: [[#row-in-state]]. You do not
need an id, and you do not need per-row state inside the `foreach`: a component member is ONE thing, and the row
travels to the action as an argument.

| Member | Declares | Evaluates |
|---|---|---|
| `T name = init;` / `var name = init;` | A client **snapshot** field | Initializer at mount; assignable — a write re-renders. Frozen until reassigned. |
| `Type name;` | A field with no initializer | Defaults to null, assigned later (e.g. an `on mount` ghost seed). |
| `var name = Entity.ToList();` | A server read, **once** (a snapshot) | Runs on the server; rows land in the shared client store, then freeze. Exposes `.items / .total / .loading / .error / .hasMore`. |
| `live var name = Entity.ToList();` | A **reactive** server query | The same read, kept current — a data-change SIGNAL (never row values) triggers a refetch. |
| `live var name = Entity.Single(x => x.Slug == slug);` | A reactive query bound to a value | The predicate may reference the component's **props** (a route param) **and its own fields / computeds** — each captured value is sent to the server read, and the query **refetches when it changes**. So a search box keys a list off a filter field, and a `[Layout]` keys a lookup off a computed slug; the query rebinds in place with no remount. |
| `live var name = Entity.Where(x => x.Ref == row).ToList();` | A reactive query keyed on a **row** the component holds | A query may compare a reference against a row another member loaded (`i.Organization == org`), and may guard on the row (`org != null && …`, `org is not null`). Both bind the row's **id** — the one member every client-held row carries — so the query sends a scalar, and an unloaded row answers **no rows** rather than a refusal. Any other member of a captured row (`org.Name`) is not sent: lift it into its own `live var` and key on that. |
| `live var name = expr;` | A tracked **computed** value | Recomputed on the CLIENT when its inputs change. It may **call a helper and allocate** — `live var shares = Settle(people, pot);` is a computed, not a query. The one thing it may not do is reach the **server**; that is the query form above. Not assignable. |
| `action name(params) { … }` | An event handler | On the client interpreter, invoked by an event prop. |
| `on change { … }` | A reactive side-effect | A tracked reaction: re-runs when a value it READ changes, to push that value somewhere OUTSIDE the component (e.g. `on change { Navigation.SetTitle(org.Name); }`). It may not assign the component's own state — that's a compile error. See [on change](https://osysharp.com/reference/ui/on-change/). |
| `on mount { … }` / `on unmount { … }` | Lifecycle bodies | Auto-invoked ONCE — `on mount` before first paint (seed a draft, kick off a load), `on unmount` at teardown (a final flush). See [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/). |
| `RetType name(params) => expr;` / `{ … }` | A plain helper method | Like an action; an expression body is a one-return function. A PURE one is callable from a render expression (`Text(Subtotal())`) — see [Calling helpers from render](https://osysharp.com/reference/ui/render-calls/). |

**A query keyed on a row is written the way C# reads it.** The guard and the comparison are both identity questions
about `org`, and the compiler keys the read on `org.Id` for you; nothing has to be lifted by hand.

```osy title="a query guarded on, and keyed on, a row another member loaded" test app=ui-component-row-key
using Osysharp.Ui;

entity Organization { [Unique, MaxLength(60)] string Slug; [MaxLength(100)] string Name; }
entity Invitation { Organization Organization; [MaxLength(200)] string Email; }

[Page("/org/{slug}/invites")]
component InvitesPage(string slug) {
  live var org = Organization.Where(o => o.Slug == slug).FirstOrDefault();
  live var invitations = Invitation.Where(i => org != null && i.Organization == org).ToList();
  render { Stack(gap: 2) { foreach (var i in invitations) { Text(i.Email); } } }
}
```

**A `live var` computed may CALL, and it may ALLOCATE.** It is an ordinary client expression that happens to be
tracked, so the whole computation can live in one helper and be *named* rather than smeared through `render`. There
is no purity budget to spend: a method that builds a `new List<T>` and returns it is a perfectly ordinary
initializer.

```osy title="a `live var` computed calling a helper that allocates" test app=ui-component-live-computed
using Osysharp.Ui;

class Share { public string Who; public decimal Amount; }

entity Person {
  [Required, MaxLength(80)] string Name;
  security { allow read, create when IsAuthenticated || IsAnonymous; }
}

[Page("/split")]
[AllowAnonymous]
[Render(CSR)]
component SplitPage() {
  live var people = Person.ToList();       // the reactive QUERY — a server read, kept current
  decimal pot = 90m;                       // an ordinary client field

  live var shares = Settle(people, pot);   // the tracked COMPUTED — it CALLS, and it ALLOCATES

  List<Share> Settle(List<Person> ps, decimal total) {
    var rows = new List<Share>();
    foreach (var p in ps) { rows.Add(new Share { Who = p.Name, Amount = total / ps.Count }); }
    return rows;
  }

  render {
    Stack {
      foreach (var s in shares) { Text($"{s.Who} owes {s.Amount}"); }
    }
  }
}
```

**The one line it cannot be is a SERVER call.** A `live var` is exactly two things — a reactive query (an entity
read, which subscribes to data changes) or a tracked computed over values the client already holds. A call that runs
on the server is neither, and the compiler says so by name: *"`live n` cannot be initialized from `PeopleCount(…)` —
that runs on the server."* Either fetch it once (`Type n; on mount { n = PeopleCount(); }`) or inline the query
(`live var n = SomeEntity.Where(…)`).

**Members share one namespace.** A component is a class, so no two of its members may share a name — a field and an
action collide just as two fields do, because both are reached as `this.name`. Declaring the same name twice is a
compile error naming both declarations, exactly as it is in C#:

```osy title="✗ a field and an action cannot share one name" syntax
component Editor() {
  int Save = 0;
  action Save() { }        // error: component 'Editor' already declares 'Save' — the field on line 2
}
```

Members that declare no name at all — `on mount`, `on unmount`, an unnamed `on change` — cannot collide, so a
component may have as many as it needs.

### What can an input write back into?   {#two-way}
Binding an input writes back through what you bound, so the target has to be something that can be *written*. Three
things are:

| target | example |
|---|---|
| an **assignable field of this component** | `string draft = "";` → `Field("Draft", value: draft)` |
| a **field of an ENTITY row** the page holds | `Field("Name", value: p.Name)` inside `foreach (var p in people)` |
| a **`Binding<T>` prop** of this component | `component WeightRow(Binding<decimal> weight)` → `NumberField(l, value: weight)` |

**And the target's type must be the `T` the control declares**, with one exception named below. A binding is
two-way: the control reads a `T` out of the target and writes a `T` back into it, so there is no conversion to
insert — a conversion would need an inverse, and one that has an inverse is the same type under another name.
`DatePicker` declares `Binding<DateOnly>`, so a `DateTime` field is a compile error, and so is widening an `int`
into a `Binding<decimal>`. The refusal names the control that binds the type you are holding (`DateTimePicker` for a
`DateTime`, `DecimalField` for a `decimal`) — call that one, or declare the field as the control's `T` and convert
wherever you SET it. A conversion written in the bind itself is not a two-way target and is refused for that instead.

#### The exception: a number in a text field   {#number-in-a-text-field}

**A number may be bound to a `Binding<string>`.** `Field` declares one, so `Field("Rate", value: rate)` over a
`decimal` is legal: the value is formatted into the box on the way out and parsed back into the member's declared
type on the way in, through that type's own exact parse. What you type is what the member holds — a number, not
text that looks like one — so `rate + 1` adds.

It is the only conversion a two-way bind admits, because a number is the only thing that answers both of the
questions a free-text box asks: *can a reader type this value*, and *does what they typed come back as the value
they meant*. A closed set fails the first — nothing stops someone typing a word that is not a member, and the write
is refused later with nothing on screen to say so — and a date fails the second, since the same day has several
spellings and the reader's culture picks one. Both stay compile errors.

```osy title="a decimal in a text field, and the control that fits it better" test app=ui-component-number-in-text-field
using Osysharp.Ui;

[Page("/rate")]
[AllowAnonymous]
[Render(CSR)]
component RatePage() {
  decimal rate = 0m;
  render {
    Stack {
      Field("Rate", value: rate);            // legal — formatted out, parsed back in
      DecimalField("Hourly rate", value: rate, min: 0m, prefix: "£");
      Text($"doubled: {rate * 2}");
    }
  }
}
```

**Reach for `NumberField` (whole) or `DecimalField` (money, a measurement, a rate) anyway.** They bind the number
directly and give the box what a text field cannot: steppers, a `min`/`max` the browser itself enforces, and the
numeric keyboard on a phone. `osy lint` says so as a SHOULD-tier finding
(`ui-text-field-bound-to-a-number`) and names which of the two fits your type. Text that is not a number at all
lands the member on the same value an empty box does, which is what those controls do with it too.

Two things are **not** targets at all, and both are compile errors rather than a silently read-only box:

- **A field of a `class` value.** A `class` is an in-memory shape with no row behind it, so there is nothing to write
  through — *"cannot two-way bind to `k.Weight` — `k` is a `class` … the edit would be read-only."* Hold the value in
  a component field and copy it into the class when you save, or make the row a real `entity`.
- **A `live var`.** It is computed, so *"there is nothing to write back into."*

This decides a data model, not a line of markup: if a page must let a person EDIT the rows of a list, those rows are
an `entity`. See [[ui-data-mutation#edit-binding]] for the entity-field form, and [generic component](https://osysharp.com/reference/ui/generic-component/) for
`Binding<T>`.

### Imperative bodies — the receiver model   {#receiver}
`action` / `on change` / `on mount` / `on unmount` / method bodies resolve **function-style with the component as the
receiver** — the same
member-body mechanism `class` methods use ([class methods](https://osysharp.com/reference/class/methods/)):

- A bare member name is an implicit-`this` member: `count = count + 1` ≡ `this.count = this.count + 1`. Both
  spellings are legal (C# scoping); locals and parameters shadow members.
- **Only a client snapshot field is assignable.** Assigning a server read, a `live var` computed, or a prop is a
  compile diagnostic (a server read is a read-only handle; a `live var` computed is derived from its inputs).
- **No `await`.** A call to a server function inside an action is a plain call — `Login(email, password);` —
  and the runtime hands off by the callee's execution side, not by a keyword. (`await` exists only for
  `Workflow.Run`.)
- Declarative slots (field initializers, `live var` computeds, render expressions) instead see members as ambient
  names — the reactive scope the renderer evaluates.

### The render tree   {#render-tree}
Statements in `render { }` are declarative nodes, persisted as the component's typed render tree:

| Form | Meaning |
|---|---|
| `Stack(gap: 2) { … }` | A platform atom. The atom set is deliberately tiny (Stack, Box, Text, Button, Pressable, Image, Input, Link) — richer surfaces come from foreign controls, never new natives. |
| `Card(p.Name)` | A call to another component. Props bind positionally / by name; the child contributes its render OUTPUT (no wrapper element). |
| `Text(expr)` | Text content — any value expression over the component scope. |
| `if (…) { } else if (…) { } else { }` | A reactive conditional chain. |
| `foreach (var x in source) { }` | Iteration over a member/prop collection or an inline query. |
| `Button("Save", onPress: Save)` | An event prop binds an `action`/method by name: `onClick`, `onInput`, `onChange`, `onSubmit`. |
| `var n = rows.Count;` | An ordinary **local**, legal wherever a render statement is — including inside a control's or an atom's child block. |
| several statements at the TOP level | A `render` block takes **many siblings**: `render { Text("a"); Text("b"); }` compiles. A `Stack`/`Box` is for LAYOUT, never to make the tree well-formed. |

**A control's content block IS a render block** — same grammar, same locals, all the way down:

```osy title="a control's child block is a render block — and render takes many siblings" test app=ui-render-block
entity Kiln { string Name; }

component Card(string title) {
  render { Stack(gap: 1) { Text(title); Slot; } }
}

[Page("/kilns")]
[Render(CSR)]
component Kilns() {
  var kilns = Kiln.ToList();

  render {
    Text("Kilns");               // MULTIPLE top-level siblings — legal, no wrapper needed
    Text("—");
    Card("In the house") {        // a control's CONTENT BLOCK is a render block…
      var n = kilns.Count;       // …so a local, an `if` and a `foreach` are all legal inside it
      Text($"{n} kilns");
      if (n == 0) { Text("none yet"); }
      foreach (var p in kilns) { Text(p.Name); }
    }
  }
}
```

### What ships to the client, and what stays on the server?   {#delivery}
- The component TREE ships to the client; expression slots ride the same wire union as function bodies — one
  expression currency, one interpreter.
- A server-read field ships only its persisted root id; the client reads the rows from the server, and a live
  refetch reads them the same way.
- A routed component is public only when it declares `[AllowAnonymous]` — a component takes **no
  `public`/`internal` modifier** (writing one is an error). Who may reach a component is an authorization question
  (`[AllowAnonymous]`, `[Composable]`, `[Authorize]`), not a [type-visibility](https://osysharp.com/reference/types/visibility/) one, so the
  type has no visibility axis to set.
- Actions run on the client against component state; entity writes go through the optimistic overlay and commit on
  the server.

### Holding ONE row in state — `Album? editing;`   {#row-in-state}
A field may hold a **single entity**, not only a list — `Album? editing;` is the ordinary way to say *which row the
dialog is for*. It is a client value like any other field (a bare field, not a server read), it compares with `==`,
and `null` is the honest spelling of "nothing selected".

⚑ **You do not need an id.** Holding `Guid editingId;` and looking the row up again on every render is the shape
people reach for when they are unsure this is allowed — it is more code, it re-finds a row you already had, and it
loses the type. Hold the row.

```osy title="which row the editor is for" test app=ui-component-row-in-state
entity Album {
  [Required, MaxLength(120)] string Title;
  security { allow read, create, update when IsAnonymous; }
}

[Page("/albums")]
[AllowAnonymous]
component Albums() {
  live var albums = Album.OrderBy(b => b.Title);

  Album? editing;                                  // one row, or none
  string draftTitle = "";

  action Edit(Album b) { editing = b; draftTitle = b.Title; }
  action Cancel() { editing = null; }

  render {
    Stack {
      foreach (var b in albums) {
        Stack(role: UiRole.Group) {
          Text(b.Title);
          Button("Edit", onPress: () => Edit(b));
        }
      }
      if (editing != null) {
        Stack(role: UiRole.Group, label: "Edit album") {
          Input(value: draftTitle, label: "Title");
          Button("Cancel", onPress: Cancel);
        }
      }
    }
  }
}
```

⚠ **A field per ROW is the thing that does not work** — `foreach` renders one component body many times over, so a
single `borrowerName` field is shared by every row and typing in one types in all of them. Two shapes are right, and
neither needs per-row state: open ONE editor at a time against a held row (above), or give the row its own
**component**, whose fields are then genuinely its own.

## Binding a click to an action — event handlers   {#handlers}
An event prop takes one of your component's **actions**. Two forms:

```osy title="naming an action, and binding one with an argument" test app=ui-component-handlers
[Principal] entity User { [Required] string Email; }

entity Item {
  [Required] string Label;
  security { allow create, read, update, delete when IsAuthenticated; }
}

[Page("/toolbar")] [Render(CSR)]
component Toolbar() {
  var rows = Item.OrderBy(r => r.Label).ToList();
  action Save() { UnitOfWork.Commit(); }
  action Remove(Guid id) { var r = rows.Single(x => x.Id == id); r.Delete(); }   // over the rows already fetched

  render {
    Row {
      Pressable(onClick: Save) { Text("Save"); }                 // no arguments — name the action
      foreach (var row in rows) {
        Pressable(onClick: () => Remove(row.Id)) { Text("×"); }  // pass an argument
      }
    }
  }
}
```

`() => Remove(row.Id)` reads like C# and behaves like it: the argument is captured **where the handler is written**.
Inside a `foreach`, each row's button carries that row's id — the × removes the row you clicked, not the last one
drawn.

The lambda takes no parameters, and its body must **call** something. A handler that doesn't call anything would do
nothing, so it's a compile error rather than a button that silently ignores you:

```osy title="✗ a handler body must CALL something" syntax
Pressable(onClick: () => count)          // error: a handler's body must CALL an action or method
Pressable(onClick: () => Remove())       // error: 'Remove' takes 1 argument(s) but 0 were given
Pressable(onClick: () => delete(row.Id)) // error: 'delete' is not an action, method, or callback parameter
```

A component that takes a **callback parameter** can forward it the same way, which is how a shared component
(a tab, a table row) reports back what happened to it. A callback parameter is spelled as a C# delegate:
`Action` for a no-argument callback, `Action<T…>` for one that carries values (`Action<string> onClose`), and
`Func<T…, TResult>` for an accessor that returns a value (`Func<Row, bool> predicate`).

```osy title="a callback parameter" test app=ui-component-handlers
[Composable] component Tab(string path, Action<string> onClose) {
  render { Pressable(onClick: () => onClose(path)) { Text("×"); } }
}
```

A callback parameter can also be **invoked from an imperative body** — an `action`, `method`, or `on change` block — not
only from a render lambda. This is what lets a component do its own work first and *then* report back. A menu that
closes itself (a state write, which needs a body) before telling its parent where to go:

```osy title="a callback with two arguments" test app=ui-component-handlers
[Composable] component Switcher(string path, Action<string, bool> onLeave) {
  bool open = false;
  action Pick(bool dirty) {
    open = false;          // close the menu…
    onLeave(path, dirty);  // …then hand the destination to the parent
  }
  render { Pressable(onClick: () => Pick(true)) { Text(path); } }
}
```

The call is checked against the delegate's parameters exactly like the render-lambda form (`onLeave(path)` alone
would be a "takes 2 argument(s)" error). A callback is fire-and-forget from a body: it hands control to the parent's
action and evaluates to nothing, so it is a statement, not a value.

### How does a caller wire up a callback?   {#composing-a-callback}
The caller supplies a callback parameter by **naming an action** — `onPick: Pick` — the same way a render lambda
names one for `onClick`. This is what makes a shared row reusable, over any element type (a `string`, an entity,
whatever the list holds): one component, composed once per item, each instance carrying its own row and reporting
back through the same callback.

```osy title="a factored row, composed once per item" test app=ui-component-handlers
[Composable] component ItemRow(Item item, Action<Item> onPick) {
  render { Pressable(onClick: () => onPick(item)) { Text(item.Label); } }
}

[Page("/items")] [Render(CSR)]
component ItemList() {
  live var items = Item.ToList();
  Item? picked = null;
  action Pick(Item item) { picked = item; }
  render {
    Stack {
      foreach (var item in items) { ItemRow(item, onPick: Pick); }
    }
  }
}
```

⚠ **The composition itself must name an action — an INLINE LAMBDA there is refused**, even though the render-lambda
one line up (`onClick: () => onPick(item)`, *inside* `ItemRow`) is exactly the shape this page opened with. The
difference is WHERE the callback is invoked from: `onPick` is called by `ItemRow`'s own `onClick`, so composing it
has to hand the platform's event system something it can call directly with the clicked row — a bound action, never
an arbitrary expression:

```osy title="✗ composing with an inline lambda instead of naming an action" syntax
foreach (var item in items) {
  ItemRow(item, onPick: it => { Pick(it); Extra(); });   // error: bind by naming an action, not a lambda
}
```

If the caller needs something `ItemRow` does not pass — which page, group or column the row sits under — that
context is the CALLER's own to carry, not something to close over: widen the delegate to
`Action<Context, Item>` and invoke it as `onPick(context, item)`, so the value travels with the clicked row
instead of being captured at composition.

## Examples       {#examples}

```osy title="catalog" test app=ui-catalog
entity Product { bool Active; string Name; }

component Card(string label) {
  render { Text(label); }
}

[Page("/catalog/{slug}")]
[Render(CSR)]
component Catalog(string slug) {
  int count = 0;
  live var label = slug;
  var products = Product.Where(p => p.Active).ToList();
  live var top = Product.OrderByDescending(p => p.Name).Take(3).ToList();

  action Increment() { count = count + 1; }
  int doubled(int x) => x * 2;

  render {
    Stack(gap: 2) {
      Text("Catalog");
      foreach (var p in products) { Card(p.Name); }
      if (count > 0) { Text("has"); } else { Text("none"); }
      Button("+", onPress: Increment);
    }
  }
}
```

## See also       {#see-also}
- [class methods](https://osysharp.com/reference/class/methods/) — the shared member-body mechanism (`this`, implicit members, visibility)
- [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/) — why a component, alone among top-level types, takes no `public`/`internal`
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — server-side waits (workflows), distinct from UI reactivity


---

<!-- https://osysharp.com/reference/ui/controls/ -->

# control — foreign UI controls (charts, grids, maps)

> A `control` block declares the contract of a foreign UI widget — a chart, a data grid, a map — that a small JavaScript module implements. You declare its typed props and events (and the shape types it consumes) in Osy#; the compiler type-checks every call site against that contract, projecting your app's data into the control's shape. Foreign controls are how you add rich, third-party widgets beyond the platform's few native building blocks.

<!-- id: ui-controls · area: ui · stability: stable · html: https://osysharp.com/reference/ui/controls/ -->

## Summary        {#summary}
The platform ships a **small set of native building blocks** (`Stack`, `Text`, `Input`, `Button`, …) and a library
of components you compose from them. Everything richer — an interactive **chart**, a sortable **data grid**, a
**map** — is a **foreign control**: a self-contained widget implemented once in JavaScript and described to the app
by a **`control` block**.

A `control` block is the widget's **contract**. It declares the widget's name, the typed **props** it accepts, the
**events** it emits, and the **shape types** its data must take. You write the block in Osy# alongside your app; the
compiler then type-checks every use of the control — so `Chart(data: …)` is verified against the control's declared
props exactly like a call to any native component, and a wrong-shaped projection is a compile error, not a runtime
surprise.

A control is always **100% owned by your app**: the platform ships none. You obtain (or write) a widget's small
adapter module, declare its contract, and reference it from your pages.

## Signature      {#signature}
```osy title="the contract block — props in, events out" syntax
control <Name> {
  contractVersion "<x.y>"           // the adapter interface this control targets
  version "<x.y.z>"                 // the control's own version (its prop/event schema)
  participation <headless|hookable|opaque>   // REQUIRED — what the platform hands this control

  props {
    <Type> <name>;                  // required by default
    [Values(a, b, c)] <Type> <name> = <default>;   // a closed value set + a default → optional
    <Type>? <name>;                 // nullable → optional
  }
  events {
    <name>(<Type> <param>, …);      // an event the control emits, with a typed payload
  }
}
```

The prop and event **types are the control's OWN shape types** — ordinary `class` declarations that live next to the
`control` block, never your app's entities:

```osy title="the control's own shape types, and why every field is public"
class Point  { public decimal X; public decimal Y; }
class Series { public string Label; public Point[] Points; }
```

Declare those fields **`public`**. A class field is private by default, and the app builds these shapes at its call
site with an object initializer (`new Series { Label = … }`) — which a private field refuses.

## Description    {#description}

### When do I need a control instead of a component?   {#why}
The platform deliberately ships **few native building blocks** and keeps richness in two places: a library of
components you compose from those blocks, and **foreign controls** for anything that needs bespoke rendering or a
third-party library. A control is the right tool when a widget:

- wraps a **third-party library** (a charting engine, a mapping SDK, a rich data grid), or
- needs **imperative, canvas- or SVG-level rendering** that composition from native blocks can't express.

Widgets that are **near-universal and security-sensitive** (Markdown rendering, whose sanitization is
correctness-critical) are native primitives instead — you don't ship those as controls. Everything else bespoke is a
control.

### What a `control` block declares — versions, participation, props   {#the-control-block}
A `control` block declares four things:

- **`contractVersion`** — the version of the adapter interface the control's implementation targets. The client
  refuses to load a control whose `contractVersion` it doesn't support, showing a placeholder instead of a broken
  mount.
- **`version`** — the control's *own* version, which moves as its author evolves its props and events. This is
  independent of `contractVersion`.
- **`participation`** — **required.** How much the control cooperates with the app's design tokens and layout, and
  with it what the platform hands the control: a `headless` or `hookable` control renders through your theme and may
  be given a credentialed channel to your app's own data, while an `opaque` one gets neither. There is no default,
  because an omission would decide that for you. One of three rungs:
  - **`headless`** — the control computes its geometry/state and the platform renders it through your **theme
    tokens**, so it inherits your colors, spacing, and dark mode automatically. Best consistency. *Preferred.*
  - **`hookable`** — the control renders its own DOM but exposes CSS-variable hooks, so it can partially adopt your
    tokens.
  - **`opaque`** — the control renders everything itself; a self-contained visual island with no token participation.
- **`props`** and **`events`** — the typed inputs the control accepts and the typed signals it emits (below).

### Props — the control's typed inputs    {#props}
Each prop is a typed field. Prop types are the control's own shape types (or scalars) — never your app's entities,
which keeps a control **reusable across apps**.

- A prop is **required** unless it has a default value or is declared nullable (`Type?`). Omitting a required prop is
  a compile error.
- **`[Values(a, b, c)]`** constrains a prop to a **closed set** of values; passing anything outside the set is a
  compile error (a typo is caught, exactly like an enum member).
- A **default** (`kind = "line"`) makes the prop optional and supplies the value used when a call omits it.

### Events — the control's typed outputs    {#events}
An `events { … }` block declares the signals the control emits, each with a **typed payload**. An app binds a handler
to an event using the **same syntax as a native component's event handler** — a control is just another render node
that emits events, and the action it binds is one of your app's own actions.

Two rules for the payload:

- **Bind the NAME of an action whose parameters match the event's payload.** The control emits a positional payload
  (`host.emit('rowSelected', …)`), which the platform maps onto your action's parameters **in order** — their names
  are yours to choose. So `rowSelected(T row)` binds to `action SelectRow(User u)`. The types must match, and the
  action may not declare more parameters than the event emits; declaring fewer is fine, and ignores the rest.
  A handler is a bare action name, never a lambda: `rowSelected: SelectRow`, not `rowSelected: id => SelectRow(id)`.
- **Emit the ROW, not a display position, when the control reorders.** A control that sorts, filters, or paginates
  changes the *display* order of rows, so a positional index no longer lines up with the data your app passed in.
  Declare such an event to carry the row itself — `rowSelected(T row)` on a generic control, or a named entity — and
  the handler receives that record: `action OpenUser(User u) { … }`. A positional index is only safe for a control
  that never reorders its items (e.g. a chart point).

  In the shim, emit the row **exactly as you received it**: `host.emit('rowSelected', row)`, where `row` is one of the
  records that arrived in your rows prop. You send the flattened record; your app's action receives the record as one
  of its own rows, read live — so a handler still sees the current values even if something changed the row after you
  were handed your props.

### Typing a control to the app's own entity — `DataGrid<T>`    {#generics}
A control can be **generic** over the app's own type: `control DataGrid<T> { … }`. The type parameter `T` binds at the
call site — inferred from the collection argument — so the app hands its **live entity list straight in** with no
projection copy, and the compiler type-checks the columns, the other props, and the event payloads against the app's
own entity:

```osy title="a generic control, and a call site" test app=ui-controls-grid
[Principal] entity User {
  [Required] string Email;
  security { allow create, read, update when IsAuthenticated; }
}

// The control's OWN shape. `public` matters: a class field is private by default, and the call site below
// builds one with an object initializer — a private field is not reachable from there.
class GridColumn {
  public string Key;
  public string Label;
}

control DataGrid<T> {
  contractVersion "1.0"
  participation headless
  props {
    T[] rows;                 // the app passes its live list — T binds to the entity here
    GridColumn[] columns;     // the control's OWN shape (a field key + a label)
  }
  events {
    rowSelected(T row);       // the row itself, not a display index
  }
}

[Page("/users")] [Render(CSR)]
component UsersPage() {
  var users = User.ToList();
  render {
    DataGrid(                 // T is inferred = User
      rows: users,
      columns: [ new GridColumn { Key = "Email", Label = "Email" } ],
      rowSelected: OpenUser
    );
  }
  action OpenUser(User u) { /* the row itself — no lookup to do */ }
}
```

**What this example needs to actually run:** the control declaration above is the *contract*; a `control` that is
rendered also needs a **bundle registered and pinned** (`osy control add …`, which writes `osyrin.lock`). Without one
the compile refuses the render — see [Pinning a kit version (using Ui@2)](https://osysharp.com/reference/ui/kit-versioning/). The fence is compiled here with a stand-in package,
because a markdown fence has no `.js` to point at.

The control declares only its *own* shapes (`GridColumn`) and the type parameter — never the app's types — so it
stays app-independent, yet every call site is checked against your real entity.

**Responsive & touch live in the shim, driven by your tokens.** A control that adapts to screen size (a grid that
renders as a table on wide screens and cards on narrow ones, with touch-sized hit targets) implements that *mechanic*
itself — but reads the **values** (the breakpoint, the tap-target floor, density) from your app's design tokens via
`host.tokens`. So the look and the responsive thresholds stay in your design system, consistent across controls; the
shim only carries the imperative rendering.

**Declarative layout — you shape both views from the call site.** A well-designed control exposes its layout as typed
props so the *app* controls how each view reads, without touching the shim. A data grid, for example, gives each
column layout hints and takes a whole-grid density; the shim honors them in **both** the table and the card view:

```osy title="layout hints as typed props" test app=ui-controls-columns
[Principal] entity User {
  [Required] string DisplayName;
  [Required] string Email;
  bool IsActive;
  security { allow create, read, update when IsAuthenticated; }
}

class GridColumn {
  public string Key;          // the field to read from each row
  public string Label;        // the column header / the card label
  public string? Align;       // cell alignment: "left" (default) | "right" | "center"
  public string? Format;      // value format: "text" | "number" | "currency" | "date" | "bool"
  public bool? Title;         // in the card view, this column is the card's heading
  public bool? Secondary;     // in the card view, this column is the card's subtitle
}

control DataGrid<T> {
  contractVersion "1.0"
  participation headless
  props {
    T[] rows;
    GridColumn[] columns;
    // The density knob the call site below passes. A prop must be DECLARED to be passable — a closed set,
    // so `density: cosy` is a compile error naming the three that exist.
    [Values(compact, comfortable, spacious)] string density = "comfortable";
  }
  events { rowSelected(T row); }
}

[Page("/users")] [Render(CSR)]
component UsersPage() {
  var users = User.ToList();
  render {
    DataGrid(
      rows: users,
      density: comfortable,                    // a closed set: compact | comfortable | spacious
      columns: [
        new GridColumn { Key = "DisplayName", Label = "Name",  Title = true },
        new GridColumn { Key = "Email",       Label = "Email", Secondary = true },
        new GridColumn { Key = "IsActive",    Label = "Active", Format = "bool", Align = "center" }
      ],
      rowSelected: OpenUser
    );
  }
  action OpenUser(User u) { /* the row itself */ }
}
```

The wide layout renders a table with those alignments and formats; the narrow layout renders one card per row, using
the `Title` column as the card's heading and the `Secondary` column as its subtitle. Nothing here is a platform
mechanism — it is ordinary typed props the control declares and its shim honors, so a different control chooses whatever
layout vocabulary fits it.

### A per-row template — writing a bespoke cell/card in Osy#    {#templates}
Flat props shape a cell; they can't compose one. When you need a **bespoke cell or card** — an avatar next to a name, a
status pill, a two-line identity — a control can accept a **builder template**: you write the row as ordinary Osy# in a
trailing block, and the platform renders it once per datum where the control asks. The block's parameter binds each
row, so the template type-checks against your entity exactly like the rest of your UI:

```osy title="a builder template per row" test app=ui-controls-template
[Principal] entity User {
  [Required] string DisplayName;
  [Required] string Email;
  bool IsActive;
  security { allow create, read, update when IsAuthenticated; }
}

class GridColumn { public string Key; public string Label; }

control DataGrid<T> {
  contractVersion "1.0"
  participation headless
  props { T[] rows; GridColumn[] columns; }
  events { rowSelected(T row); }
}

[Composable] component Badge(bool on) {
  render { Text(on ? "Active" : "Inactive"); }
}

[Page("/users")] [Render(CSR)]
component UsersPage() {
  var users = User.ToList();
  render {
    DataGrid(rows: users, columns: [ new GridColumn { Key = "DisplayName", Label = "Name" } ]) { u =>
      Stack {                                          // u binds to each User row
        Text(u.DisplayName);
        Text(u.Email);
        Badge(u.IsActive);
      }
    }
  }
}
```

The template is **yours** — any component, atom, binding, or action-bound handler works inside it, and `u.Bogus` is a
compile error just like anywhere else. The control decides only **where** each row's content sits (a table cell, a card
body); the platform renders **what** you wrote. This is the escape hatch beyond flat props: a control that supports a
template documents which region it renders it into (a grid, for instance, uses it as the card body on narrow screens
while the table view still follows the column hints).

**Where the datum's type comes from.** On a generic control (above) it is `T`, bound from the argument you passed. On a
control that names its row type outright, it is the element type of the **collection prop** the control declares —
`props { Row[] rows; }` makes the datum a `Row`. Either way the datum has a real type, which is what makes a typo
inside the template a compile error.

A control declaring **two** collection props is refused when you write a template against it: the datum could be a row
of either, and a template that silently type-checked against the wrong rows is worse than being asked. Make such a
control generic so the call site says which rows the template is for.

Two things to know:

- The template is a function of the **row datum** (plus your component's actions). A cell that reads component *state*
  won't re-render when that state later changes — pass what the cell needs as row data, or drive interaction through an
  action.
- A control exposes **one** template (the trailing block). It's the row/card template; richer controls with multiple
  named template regions are a later addition.

### Named slots — one template per column, lane, or field    {#slot-vocabulary}
Some controls accept **more than one** template, and the names aren't fixed by the control: they're decided at the call
site. A data grid takes one template per **column**; a board takes one per **lane**; a form builder one per **field**.
A control says where those names come from with one line:

```osy title="where the slot NAMES come from — one line on the control" syntax
control DataGrid<T> {
  participation headless
  props { T[] rows; GridColumn[] columns; }
  slots from columns.Key            // the slot names ARE the Keys of whatever `columns` is passed
}
```

`slots from <prop>.<member>` names a prop and the member of its elements that spells each slot name — and that is all
the platform learns. It never learns what a column *is*; a board writes `slots from lanes.Id`, a form builder
`slots from fields.Name`, and the same machinery checks all three. Callers then write one `slot <Name> { row => … }`
per name:

```osy title="the call site decides the names, then fills one" syntax
DataGrid(rows: files, columns: [
  new GridColumn { Key = "Name",    Label = "Name" },
  new GridColumn { Key = "Size",    Label = "Size", Align = "right" },
  new GridColumn { Key = "Actions", Label = "" }        // renders no field — it's a place to put buttons
]) { f =>
  Stack { Strong(f.Name); }                             // the default row/card template

  slot Actions { f =>                                   // one named template, for the Actions column
    Pressable(onClick: () => DeleteFile(f.Id)) { Icon(Icons.Trash); }
  }
}
```

**A slot name is checked against the columns that call passes**, so `slot Actons` is a compile error that lists the
names you did pass. Note what this does *not* require: `Actions` is not a field of the row and never could be — a
column that exists to hold buttons is as legitimate as one that shows data, and it gets to be called what it is.

Declaring `slots from` is optional. A control that omits it accepts slot names unchecked — the platform won't invent a
vocabulary it wasn't given. Checking is also skipped where the names can't be read at the call site (a `columns:` built
at runtime rather than written as a literal list), because a name the compiler couldn't read is not the same as a name
that isn't there.

### Calling a control from a page — projecting your data in   {#using}
Call a control like any component. The app **projects its own data into the control's shape** at the call site, and
binds handlers to its events:

```osy syntax
component SalesDashboard() {
  var sales = Sale.ToList();
  render {
    Chart(
      data: sales.Select(s => new Series { Label = s.Region, Points = s.Trend }),
      kind: bar,
      pointSelected: DrillInto
    );
  }

  action DrillInto(int index) { /* … */ }
}
```

This fragment shows only the call. **To run it you also need** the `Chart` control declared (its props and events are
what the call is checked against), the `Series`/`Point` classes it names — with `public` fields, or the projection
above cannot reach them — a `Sale` entity, and a registered bundle for the control. The [Examples](#examples) section
below carries all of that as one compiled unit.

The compiler checks the call site against the control's contract and reports, at compile time:

- an **unknown** prop or event name (`Chart(bogus: …)` — the contract is authoritative);
- a **missing required** prop (`Chart(kind: bar)` with no `data`);
- a **`[Values]`** violation (`kind: circle` when the set is `line, bar, scatter`);
- a **result-type mismatch** — a scalar where the control wants a shape, or the *wrong* shape (a `Point[]` where it
  declares `Series[]`); the projection is checked against the declared prop type;
- an **event handler** whose parameters don't match the event's declared payload (too many parameters; a body that
  reads an undeclared name).

A single-element-vs-list distinction on an otherwise-correct shape is not yet enforced; every other mismatch above is.

### The implementation — the JavaScript shim    {#implementation}
A control is implemented by a **small, framework-agnostic JavaScript module** — the "shim", a thin adapter over a
third-party library (a charting engine, a data grid) or a hand-written widget. It exports one function:

```js
// grid.js — the shim (you bundle it with esbuild into one self-contained module)
export function mount(el, props, host) {
  // el     — the element to render into
  // props  — the app's projected data, prepared by the platform
  // host   — what the platform offers this control (below)
  draw(el, props, host);
  return {
    update(nextProps) { draw(el, nextProps, host); },   // re-flowed when the app's data changes
    destroy()         { /* release listeners / timers / observers */ },
  };
}
```

A control whose start **continues after `mount` returns** — a document to fetch, a chunk to load — also returns
**`ready`**, a promise that resolves once it is running and **rejects when it is not**. The platform fails a control
whose `ready` rejects exactly as it fails a `mount` that threw, so a test's probe reports *never started*, with the
reason, instead of answering from chrome drawn over nothing. Omit `ready` when `mount` is the whole start.

The **`host`** the platform passes in:

- **`host.emit(event, …payload)`** — raise one of your declared events; the platform runs the app's bound handler.
  For `events { rowSelected(T row) }` you call `host.emit('rowSelected', row)` with the record you were handed.
- **`host.tokens`** (present for a `headless`/`hookable` control) — the app's design tokens, so the control renders
  THROUGH the app's theme (and dark mode) rather than hardcoding colors:
  - `host.tokens.cssVar('colors.border')` → `var(--colors-border)` — use it directly in a style (stays live).
  - `host.tokens.get('colors.border')` → the current resolved value — for a decision in JS.
  - An `opaque` control gets no `host.tokens` (it's a self-contained visual island).

Your `mount` **must return `{ update, destroy }`** — the platform calls both, and it checks the shape at the mount, so
a forgotten `return` is reported against the mount rather than surfacing later as a puzzling `update` failure.

The platform wraps every control in an **error boundary** (a throw from `mount`/`update`/`destroy` degrades to a
placeholder — never takes the page down) and a **`contractVersion` gate** (a control whose `contractVersion` this
platform doesn't support shows a placeholder, not a broken mount).

**Never write to DOM the wrapped library owns.** If your shim adapts a library that renders and re-renders its own
nodes — an editor, a virtualised grid, a canvas scene graph — setting a class or an attribute on one of those nodes
works, and then silently stops working: the library redraws, and your change leaves with the node it was on. Nothing
throws and nothing logs; the feature simply does not happen, intermittently, depending on whether a redraw follows.

Use the library's own mechanism for saying *"this node looks like this"* — a decoration, a cell renderer, a class hook
— because that is re-applied on every redraw by definition. Own the DOM you created (`el` and its children); treat
everything the library created as read-only.

### What a prop actually looks like in JavaScript    {#prop-shapes}
A shim reads `props[name]` directly, so it matters exactly what shape arrives. There is one rule, and it is worth
learning once:

> **Containers are flattened. Values are not.**

A **container** is something whose fields a plain JS module could not otherwise reach — a queried row, a query result,
a `class` instance. Each is flattened to a plain object with readable own-properties, all the way down:

| The Osy# prop | What `props[name]` is |
|---|---|
| an entity (`Order`) | `{ id, ...fields }` — `id` is the row's id |
| a list / query (`Order.ToList()`) | an **array of those records** — never a query handle |
| a `class` instance | a plain record; nested classes flatten too |
| a `Collection` (child rows) | **absent** — child rows load through their own query |

A **value** arrives exactly as the runtime holds it. In particular, the exact types stay **boxed objects**, not
primitives — converting them here would destroy the precision they exist for:

| The Osy# type | What you get | Reading it |
|---|---|---|
| `decimal` | an exact decimal object | `total.toString()` — never `Number(total)` |
| `long` | an exact 64-bit integer object | `seq.toString()` |
| `DateTime` / `DateOnly` / `TimeOnly` / `TimeSpan` | the matching exact object | `.Year`, `.toString()` |
| `int` / `double` / `bool` / `string` | a JS number / boolean / string | directly |
| `Guid`, or a reference to another record | a `string` id | directly |
| `Json` / `RichText` | a **string containing JSON**, not an object | `JSON.parse(v)` |
| binary | a `Uint8Array` | directly |

The `Json` row is the one that surprises people: it looks like it should already be an object, and it is not.

**A prop that the call site may omit is genuinely ABSENT.** Only the props an app actually wrote are sent, so a
nullable prop and a prop with a default both arrive as `undefined` when the call leaves them out — **a default is not
delivered**; it is the call site's licence to omit the argument, and the shim supplies the value:

```ts
const density = props.density ?? 'comfortable';   // not defensive — this is the contract
```

The generated types say so: such a prop is declared optional, so reading it without a fallback does not compile.

If a shim wants text, it asks for text (`value.toString()`). The platform does not decide that for you — a control
that renders a total to two places and one that renders it to four are both legitimate, and only the control knows
which it is.

#### Enum fields — the key, plus the word    {#enum-labels}
An enum-typed field keeps its **stored key**, and its display label rides alongside under `$labels`:

```js
row.Status            // 1        — the stored key
row.$labels?.Status   // "Shipped" — the word the app declared
```

Both, and in that order, on purpose. The key is what app code compares against (`o.Status == OrderStatus.Shipped`), so
overwriting it with the word would make that comparison silently false. A shim that wants to show a word reads
`row.$labels?.[key] ?? row[key]`. When a row has no enum fields there is no `$labels` key at all — absent rather than
empty, so "this row has no enum" is distinguishable from "the platform supplied nothing".

### Generated types for your shim    {#typings}
`osy control build` writes a TypeScript declaration file beside each control's bundle —
`model/controls/<name>.control.d.ts` — generated from that control's `control` block by the same compiler that checks
the call site. It is **checked in**, so your editor sees it with no build step and no toolchain, and it needs no
bundler to produce (generating it is pure compilation).

Import it in your shim and the ABI stops being folklore:

```ts
import type { FolderNode, FolderTreeHost, FolderTreeProps, ControlHandle } from './tree.control.js';

export function mount(el: HTMLElement, props: FolderTreeProps, host: FolderTreeHost): ControlHandle<FolderTreeProps> {
  const first: FolderNode = props.nodes[0];
  host.emit('folderSelected', first.FolderId);   // typed from your `events` block
  return { update(next) { /* … */ }, destroy() { /* … */ } };
}
```

What it gives you:

- **`<Name>Props`** — every prop in the shape it actually arrives in (see [[#prop-shapes|What a prop actually looks
  like]]), with the `.osy` spelling in a doc comment beside each one.
- **`<Name>Host`** — with **`emit` typed from your declared events**, one overload each. An event name you never
  declared, or the right name with the wrong number of arguments, is a compile error in your editor rather than a
  silence at every layer. This is the single most valuable thing on the page: that exact drift has shipped before.
- **The ABI types** (`TokenScope`, `SlotHandle`, `ControlHandle`) pinned to your control's `contractVersion`. They
  ride with the declarations instead of coming from a package, because a package can drift from the platform that
  compiles your call site — which is the skew `contractVersion` exists to prevent.
- **`EntityRow`**, for a generic control — the row shape an app binds `T` to, carrying `id` and `$labels`.

**Do not edit it by hand.** Editing it does not change what the platform sends your shim; it only makes your editor
disagree with the runtime. `osy control build --check` fails when the file on disk differs from what the declaration
would produce, so a stale copy is caught in CI rather than in a browser. Generation is deterministic, so a clean tree
never sees that error.

### If your shim raises an event the control never declared    {#emit-conformance}
The platform checks this in three places, so it is caught wherever you are working:

1. **In your editor**, if your shim is TypeScript — `host.emit` is typed from your `events` block, so a wrong name or
   the wrong number of arguments will not compile. See [[#typings|Generated types for your shim]].
2. **At `osy control validate`** — the bundle is scanned for `emit("name")` and every literal name is checked against
   your declaration. This works on minified output and needs no JavaScript toolchain. A **dynamic** name
   (`emit(kind)`) is skipped rather than guessed at.
3. **At runtime on a dev server** — the control's actual emits are checked against the declaration, including the
   dynamic names the scan could not read, and the real argument count. It warns; it never kills a working control.

An undeclared event is not a style problem. The app can only bind events the control declares, so an undeclared one
is raised into the void — the control believes it reported something and nothing is listening.

### How do I ship a control with my app? — bundle, validate, register   {#packaging}
A control is **vendored into your app** (version-controlled with it), not fetched at runtime — so the running page
stays same-origin. The workflow:

1. **Bundle** the shim with esbuild into one self-contained module (`grid.js`) — this pulls in its third-party deps.
   Either run esbuild yourself, or let [[#building|`osy control build`]] run it for you.
2. **`osy control validate grid.js`** — the app-neutral API check: statically confirms the bundle exports the ABI
   (`mount`), that its sibling `control` block (`grid.osy`) parses and declares a participation rung, and that it's
   within the size cap. It runs no JavaScript, so it checks the surface — not the running behaviour.
3. **`osy control add grid.js`** — registers it into *this* app: the bundle and its `control` block are
   vendored into your project (under `model/` by default, so the block compiles like any model source), and the
   bundle's content hash is pinned in `osyrin.lock`. Add `--source shim.ts` to record what the bundle was built
   FROM, which is what makes it rebuildable below.
4. On the next **compile** the registered package **rides the compile** — uploaded **content-addressed**
   (the server dedups, so an unchanged package re-uploads as a no-op) and served **same-origin** with an immutable
   cache; a package no control references any more is garbage-collected.

### Authoring a shim from an npm library    {#authoring}
You can wrap **any** framework-agnostic JavaScript library in a shim — a charting engine, a data grid, a **tree**. The
library is **bundled INTO** the shim (inlined by esbuild), so the running page loads one self-contained same-origin
module and never fetches from a CDN. The recipe, end to end:

1. **Write the shim in TypeScript**, importing the library and exporting `mount(el, props, host)` (the ABI above). It
   projects `props` into the library, renders the DOM through `host.tokens`, and calls `host.emit(...)` for events.
   **The shim belongs to your app** — source and bundle both live under `model/controls/`, beside the `control` block
   they implement. See the shipped examples in `demo/file-manager/model/controls/`: `grid.ts` (TanStack Table) and
   `tree.ts` (a folder tree over `@headless-tree/core`).
2. **Install the library** and **bundle** with esbuild — one command inlines the library into the shim:
   ```bash
   npm install --save-dev @headless-tree/core esbuild     # in your app, beside its model/
   esbuild model/controls/tree.ts --bundle --format=esm --minify --outfile=model/controls/tree.js
   ```
   `--bundle` pulls the library's code into the output; `--format=esm` matches the loader; `--minify` keeps it under
   the size cap. *You* run this — the platform never runs your build. The library is **your app's** dependency: the
   platform client ships no third-party control code, so nothing is inherited from it.
3. **Validate + register** as above: `osy control validate tree.js` (static ABI check — exports `mount`, its sibling
   `tree.osy` `control` block parses, size cap), then
   `osy control add tree.js --source model/controls/tree.ts` (vendors the bundle + block under `model/`, pins
   the content hash in `osyrin.lock`, and records the source so you never have to run that esbuild line by hand
   again). The next compile ships it.

### Rebuilding a shim — `osy control build`    {#building}
Once a control records a `--source`, one command rebuilds it and re-pins the new hash:

```bash
osy control build            # bundle every control that has a source, re-pin what changed
osy control build --watch    # ...and keep doing it as you edit
```

That replaces the loop of running esbuild, hashing the output, and editing `osyrin.lock` by hand — miss the last step
and the next compile refuses the stale pin. The rebuilt bundle is re-validated before anything is pinned, so a shim
that stops exporting `mount` fails *here*, at build, rather than as a control that renders nothing in the browser.

`build` uses **your** esbuild — the project's own `node_modules/.bin/esbuild` first (so a pinned version wins), then
your `PATH`. Nothing is downloaded or installed on your behalf. If there is no esbuild, `build` tells you how to
install it and how to carry on without it; every other command, `add` included, keeps working with no JavaScript
toolchain at all.

A control registered from a prebuilt bundle simply has no source to rebuild, and `build` leaves it alone.

The `control` block declares the contract the page compiles against; the bundled `.js` is the implementation that
mounts at runtime. Keep the two in sync — a prop/event you add to the block must be honored by the shim, and vice versa.

### The shim's stylesheet — a real `.css` file    {#stylesheet}
A control that needs CSS of its own puts it in a **`.css` file next to the shim** and imports it. `build` inlines the
file as **text**, so the shim receives its contents as a string and injects them once:

```ts syntax
import CONTROL_CSS from './my-control.css';

function ensureStyles(doc: Document) {
  if (doc.getElementById('my-control-styles')) return;
  const style = doc.createElement('style');
  style.id = 'my-control-styles';
  style.textContent = CONTROL_CSS;
  doc.head.appendChild(style);
}
```

TypeScript needs to be told what a `.css` import is — one declaration, once per project:

```ts syntax
declare module '*.css' {
  const css: string;
  export default css;
}
```

**Why text rather than a stylesheet the page links.** A control is served as exactly one artifact — its bundle — so a
separate `.css` emitted beside it would be a file nothing ever loads. Inlining is what makes the import mean something.

**Prefer this to a template literal in the shim.** CSS held in a `` ` ``-quoted string is a string first and a
stylesheet second: a backtick anywhere in it — *including inside a comment* — ends it. An odd number breaks the build
somewhere unrelated; an **even** number builds clean and silently rewrites the CSS, because the text between them stops
being string content and becomes JavaScript. In a `.css` file a backtick is an ordinary character, and your editor
knows what the file is.

Write the CSS against the app's theme tokens (`var(--colors-surface)`, `var(--radius-md, 10px)`) so the control
inherits the app's look and its dark mode, and declare the control's own geometry as [styles — a control's own look knobs](https://osysharp.com/reference/ui/control-styles/) knobs
rather than as literals.

### The edit loop — `osyrin dev` rebuilds and recompiles for you    {#dev-loop}
Keep `osyrin dev` running while you work on a shim. It watches every control source pinned in `osyrin.lock`, and on
each save it **rebuilds the bundle, re-pins its hash, and recompiles the app** into the running server:

```console
✓ FolderTree rebuilt + re-pinned (230717a61074… → 963970d1c350…)
  control rebuilt — recompiling…
✓ recompiled — refresh the page to pick it up
```

Refresh the page and you are looking at your edit. What is gone is the ceremony: bundle by hand, hash the file, edit
the lock, recompile — four steps with a compile error waiting at the end of any one you forgot.

**When `osyrin dev` is not running**, `osy control build --watch` does the same rebuild and re-pin on its own. It
cannot recompile — there is no server to compile into — so it tells you to run `osy compile` when you are ready. Use
it when the dev server has exited (it stops after its idle keep-alive window) or when you are working on a shim
without a server up.

A save that changes nothing the bundler emits — a comment, a reformat — is detected and skipped, so a keystroke-save
does not trigger a compile.

**The browser is not reloaded for you.** After a recompile, refresh the page yourself.

### My control mounted but drew nothing — the dev diagnostics   {#diagnostics}
On a local dev server the platform watches the two failures a shim can produce without erroring, and says so in the
console. A deployed app never shows either — they are for the person writing the shim.

**"mounted but produced no DOM."** The control was handed correct data, threw nothing, and painted nothing. Nearly
always a shim that never started its library's own lifecycle. Checked after the current task drains, so a shim that
fetches before painting is not accused while it is still working.

**"re-entered `host.<method>` N levels deep."** The shim called back into the platform from inside a call the platform
had not yet returned from, and kept going — typically a shim reacting to its own change (its `setState` runs the app's
handler, which re-renders, which updates the shim, which calls `setState`). It recurses until the stack dies, far from
the line that started it. Break the cycle by making the callback asynchronous, or by ignoring a prop update the shim
itself caused.

Rendering many rows is *not* this: calling `host.slot.render(...)` once per row is breadth, and each call finishes
before the next begins. The warning fires only on a call that re-enters its own still-open frame, and only once the
nesting is far deeper than any real layout — so a control that legitimately renders children inside a parent stays
quiet.

Both also appear on the control's entry in `__osy.controls`, alongside the last props that crossed the ABI and the
events it raised — including any it raised that the app bound no handler for, which from the outside looks exactly like
never having emitted at all.

## Examples       {#examples}

Declaring a chart control and its shape types:

```osy title="the control's contract" test app=ui-controls-chart
// `public` on every field: the app builds these at its call site with an object initializer, and a class
// field is private by default.
class Point  { public decimal X; public decimal Y; }
class Series { public string Label; public Point[] Points; }

control Chart {
  contractVersion "1.0"
  version "2.1.0"
  participation headless
  props {
    Series[] data;                              // required
    [Values(line, bar, scatter)] string kind = "line";   // optional, closed set
    string? xLabel;                             // optional
  }
  events {
    pointSelected(int index);
  }
}
```

Using it, with the compiler checking the projection and the handler:

```osy title="projecting the app's data into it" test app=ui-controls-chart
[Principal] entity Analyst { [Required] string Email; }

entity Sale {
  [Required] string Region;
  decimal Amount;
  security { allow create, read, update when IsAuthenticated; }
}

[Page("/sales")] [Render(CSR)]
component SalesDashboard() {
  var sales = Sale.ToList();
  render {
    Chart(
      data: sales.Select(s => new Series { Label = s.Region, Points = [] }),
      kind: bar,
      pointSelected: DrillInto
    );
  }
  action DrillInto(int index) { /* open the region detail */ }
}
```

Mistakes the compiler rejects:

```osy title="✗ the calls the compiler rejects, and what each says" syntax
Chart(kind: bar);              // error: 'Chart' requires prop 'data'
Chart(data: sales, kind: pie); // error: 'kind' must be one of: line, bar, scatter
Chart(data: 42);               // error: 'data' expects Series[] but got int
Chart(data: sales, whatever: 1);            // error: 'Chart' has no prop or event 'whatever'
Chart(data: sales, pointSelected: index => DrillInto(index)); // error: a control event handler must NAME an `action` — write `pointSelected: DrillInto`
Chart(data: sales, pointSelected: DrillInto);   // error (if DrillInto takes 2 params): 'pointSelected' emits 1
```

A **shim** (`grid.js`) — a headless control that renders through the app's tokens and emits an event. It implements a
`control DataGrid<T> { props { T[] rows; Column[] columns; } events { rowSelected(T row); } }`, not the `Chart` above;
it is here because a grid shows the row-payload rule in action. This is a real, framework-agnostic module; a
production one typically wraps a library (e.g. a data grid or chart engine):

```js
export function mount(el, props, host) {
  const draw = () => {
    const table = document.createElement('table');
    // Render THROUGH the app's tokens (falls back gracefully when a token isn't declared):
    table.style.color = host.tokens?.cssVar('colors.onbg') ?? 'inherit';
    for (let i = 0; i < (props.rows ?? []).length; i++) {
      const tr = document.createElement('tr');
      // Emit the ROW, not `i` — the grid sorts, so a display position stops matching the app's data.
      tr.onclick = () => host.emit('rowSelected', props.rows[i]);   // → the app's `action Open(User u)` runs
      for (const c of props.columns ?? []) {
        const td = document.createElement('td');
        td.style.borderBottom = `1px solid ${host.tokens?.cssVar('colors.border') ?? 'currentColor'}`;
        td.textContent = String(props.rows[i][c.key] ?? '');
        tr.appendChild(td);
      }
      table.appendChild(tr);
    }
    el.replaceChildren(table);
  };
  draw();
  return { update(next) { props = next; draw(); }, destroy() { el.replaceChildren(); } };
}
```

### Adding an interaction — e.g. double-click    {#extending}
A control's events are its own contract, so adding a new interaction is a change to the control (which you own — it's
vendored in your app), then a binding in the page. To add double-click that opens a row, three small edits:

```osy title="step 1 — declare the event, carrying the ROW not a position" syntax
// 1. Declare the event on the control — carry the ROW, not a position (the grid sorts):
control DataGrid<T> {
  participation headless
  props  { T[] rows; Column[] columns; }
  events { rowSelected(T row); rowDoubleClicked(T row); }
}
```
```js
// 2. Emit it from the shim — the row exactly as you received it:
tr.onclick    = () => host.emit('rowSelected', row.original);
tr.ondblclick = () => host.emit('rowDoubleClicked', row.original);
```
```osy title="step 3 — bind an action whose parameters take the payload" syntax
// 3. Bind an Osy# action in the page (the payload maps onto its parameters in order):
component Orders() {
  var orders = Order.ToList();
  action Open(Order o) { /* navigate / open a detail */ }
  render { DataGrid(rows: orders, columns: [...], rowDoubleClicked: Open); }
}
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the native components a control sits alongside in a render tree
- [Slot (child content)](https://osysharp.com/reference/ui/slots/) — how a control participates in layout via a slot
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the theme tokens a `headless` control renders through
- [styles — a control's own look knobs](https://osysharp.com/reference/ui/control-styles/) — a control's OWN look knobs (`styles { }`), and the two ways an app overrides them
- [commands — the verbs a control accepts](https://osysharp.com/reference/ui/control-commands/) — the verbs a control accepts (`commands { }`), the mirror of its events
- [probe — what a control says about itself](https://osysharp.com/reference/ui/control-probe/) — what a control says about ITSELF (`probe { }`), so a test can ask a control it did not write
- [chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/) — assets a control loads on demand (`chunks { }`), so a heavy optional feature costs nothing on the pages that never use it
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — the versioned component-library model (a control is distributed per-app, not as a kit)


---

<!-- https://osysharp.com/reference/ui/data-mutation/ -->

# creating & saving data

> A UI `action` creates, updates and deletes data by writing `new Entity { … }`, assigning fields, and calling `.Delete()`. Edits apply instantly and stay visible while the user keeps working — the page's own queries read them back before anything is saved. `UnitOfWork.Commit()` sends the accumulated edits to the server atomically. A **form** commits once, on Save; a page that saves **per action** — ticking a to-do IS the save — commits in each verb. Both are correct; what is never correct is a page with no Save and no `UnitOfWork.Commit()`, where the write is discarded with no error.

<!-- id: ui-data-mutation · area: ui · stability: stable · html: https://osysharp.com/reference/ui/data-mutation/ -->

## Summary        {#summary}
A component **writes data** from an `action`: create a row with `new Entity { … }`, set fields by assignment, then
call **`UnitOfWork.Commit()`** to persist. The mutations apply **optimistically** — they take effect on the client the instant
the action runs, so the UI updates with no round-trip — and `UnitOfWork.Commit()` flushes them to the server, which validates
and persists them **atomically**. If the server rejects the write, the optimistic edit rolls back.

```osy syntax
action Save() {
  new Note { Title = title };   // create — applied optimistically on the client
  UnitOfWork.Commit();          // persist to the server (atomic); on failure, rolls back
}
```

A **form** is this action plus inputs bound to state: the user types, state updates, the action reads that state
into the new row.

### Where `UnitOfWork.Commit()` goes — the one decision {#where-commit-goes}

There are two shapes, and a page is one of them:

| shape | example | where `UnitOfWork.Commit()` goes |
|---|---|---|
| **form** — nothing persists until Save | an edit screen with a Save button | once, in the Save action |
| **per action** — the act IS the save | ticking a to-do, archiving, deleting a row | in each verb |

Both are correct and the compiler does not ask you which one you meant — **you choose by asking whether the user
should be able to change their mind before anything is stored.** If yes — an edit screen, a form with several
fields, anything a Cancel makes sense on — the page holds its edits and one Save commits them. If no, commit in the
action.

The unit of work is the DEFAULT, and it is the better model whenever the choice is close: a person can add, close
and delete several things and then decide, and `UnitOfWork.Discard()` throws the pending edits away without leaving
the page. That is the shape `admin` is built on.

> ⚠ **Do not silence it by removing the `UnitOfWork.Commit()` on a page that has no Save.** A function called with an **entity**
> argument runs inside the calling page's unit of work rather than its own, so its write becomes durable only when
> that page commits — and if nothing does, it is discarded when the page goes away, with no error and no failed
> request. The optimistic overlay renders the change, so the screen looks right. `osy lint` catches this shape as
> `data-write-never-committed`.
>
> A function taking only **scalars** runs standalone and commits in-band, which is why a `Create(string title)` verb
> persists with no `UnitOfWork.Commit()` of its own while `Toggle(Item i)` next to it does not. That difference appears nowhere
> in the source of either — it is the argument type that decides.

## Signature      {#signature}
```osy syntax
new Entity { Field = value, … };   // create a row (optimistic); evaluates to its id
var e = new Entity { … };          // …bind the id to update or reference it
e.Field = value;                   // update a field (optimistic)
e.Delete();                        // delete a row (optimistic) — the row vanishes from the page's queries at once
UnitOfWork.Commit();               // persist all pending edits to the server, atomically
```

### Deleting a row        {#delete}

`.Delete()` on the row. It joins the page's unit of work exactly like an edit does, and lands when **Save** does —
so the delete action itself has no `UnitOfWork.Commit()` in it, the same as an "Add item" action does not:

```osy title="delete a row — it lands with the page's Save, like any other edit" test app=ui-data-mutation-delete
using Osysharp.Ui;

entity Note {
  [Required, MaxLength(200)] string Title;
  security { allow read, create, update, delete when IsAnonymous || IsAuthenticated; }
}

[Page("/notes")]
[AllowAnonymous]
[Render(CSR)]
component Notes() {
  live var notes = Note.ToList();
  string draft = "";

  action Remove(Note n) { n.Delete(); }          // the delete joins the page's unit of work
  action Add() { new Note { Title = draft }; draft = ""; }
  action Save() { UnitOfWork.Commit(); }         // …and lands here, with everything else

  render {
    Stack {
      Field("Title", value: draft);
      Button("Add", onPress: Add);
      foreach (var n in notes) {
        Row { Text(n.Title); Button("Remove", onPress: () => Remove(n)); }
      }
      Button("Save", onPress: Save);
    }
  }
}
```

⚠ **A per-row action that commits is a different design, and it needs no ceremony.** If a page genuinely has no
Save — a list where pressing the bin is the whole interaction — then `Remove` commits, and that is all there is to
write:

```osy title="the other design — a page with no Save, where the press IS the save" syntax
action Remove(Note n) { n.Delete(); UnitOfWork.Commit(); }
```

⛔ **What you must NOT do is drop the `UnitOfWork.Commit()` from that shape.** On a page with no Save the write then
sits in a unit of work nothing commits, and is discarded when the page goes away — with no error, no failed request
and a screen that looks exactly right, because the optimistic overlay rendered it. That is the one failure here you
cannot see for yourself, and it is why `data-write-never-committed` is a MUST rule in `osy lint`.

It is **optimistic like the others**: the row leaves the page's queries immediately, before the server has been
asked, and comes back if the commit fails. So a list re-renders without it at once and nothing has to be re-fetched.

⚠ **There is no `Delete` on the entity TYPE** — no `Note.Delete(id)`. You delete a row you are holding, which is
what a page always has: the `foreach` variable, or an entity-typed parameter passed to the action. That is the same
rule as updating, where you assign to `n.Title` rather than calling a setter on `Note`.

⚑ **Deleting several ROWS THE PAGE HOLDS is a loop**, and it is still one commit: the unit of work is what makes
them atomic, so all of them land or none does. Deleting **by predicate** — "every stale order", rows the page never
loaded — is the set-based terminal instead: `Order.Where(o => o.Status == "Stale").Delete()` runs one statement in
the database, immediately, outside the page's unit of work, and answers how many went. Same split for updates
(`.Update(o => { … })`) and per-row creates (`.Insert(s => new T { … })`). See [Delete](https://osysharp.com/reference/query/delete/), [Update](https://osysharp.com/reference/query/update/)
and [Insert](https://osysharp.com/reference/query/insert-from/) — and note the refusal that keeps the two models honest: a bulk verb will not run while
the page's unit of work holds uncommitted changes of the same type, because a statement over stored rows cannot see
them.

### Why it hangs off `UnitOfWork` {#why-the-receiver}

The save is spelled on a receiver — `UnitOfWork.Commit()`, not a bare `commit()` — because the receiver is the point.
A page's edits accumulate in one **unit of work**, and the single most common mistake is not knowing that: writing to
an entity and never committing, or committing in every action because each one looked like a separate save. Naming
the unit of work at every call site puts the thing you are committing in front of you while you write it.

There is no lowercase carve-out to remember: everything you declare and everything you call is PascalCase, and the
compiler says so if it drifts (`ACTION_NAME_NOT_PASCAL_CASE`). Parameters and component props stay camelCase
(`Guid id`, `tone`), as do the platform's own event props (`onClick`).

## Description    {#description}
A page's data edits accumulate in an **optimistic overlay** — one unit of work for the whole page (or tab). Edits
from *every* action land in that same overlay and stay there, visible, until the user decides to save:

- **`new Entity { … }`** creates a row locally and evaluates to its id. It's visible immediately — to the rest of
  the action (its fields read back through the overlay), and to the **page's own `live var` / `foreach`** (they read
  *through* it). Add ten items across ten clicks and all ten show up, before anything is saved.

  ⚠ **One read does not see it: a new `Entity.Where(…)` issued inside an action.** That is a SERVER read — the filter
  never leaves the server — so it answers over committed data only, and a row you created a line earlier is not in it.
  The runtime refuses that line rather than handing back a wrong count. Ask the page's `live var`, which already holds
  the rows including the pending ones, or `UnitOfWork.Commit()` first and then read.
- **`e.Field = value`** records a field change on an existing (or just-created) row; reads reflect it at once.
- **`UnitOfWork.Commit()`** sends everything accumulated so far to the server, which applies your app's validation and
  security rules and persists it in one atomic step, then confirms it back as settled data.
- **`UnitOfWork.Discard()`** throws that accumulation away instead — the edits roll back and the screen returns to
  what the server last confirmed. The page stays open; only its pending changes go.

Both read the same in a server function and in a UI action, and mean the same thing: `UnitOfWork` is the accumulated
work, `Commit` persists it, `Discard` drops it.

**Where `UnitOfWork.Commit()` belongs — two places, never more.** Committing is a *user* decision, so put `UnitOfWork.Commit()` on:

1. a dedicated **Save** action (a Save button), and
2. a **discard-guard** — when the user is about to leave unsaved work (closing a tab/dialog, navigating away).

Do **not** sprinkle `UnitOfWork.Commit()` through ordinary actions. An "Add item" action just creates the row; the user's
Save is what persists the batch. This keeps drafts editable and cancellable, and matches how the server already
lets a function read its own uncommitted writes. A commit that fails (validation or permission) **reverts** the
overlay, so the UI never shows data the server rejected. A create obeys the entity's write permissions — the acting
user must be allowed to create the entity.

### Throwing changes away — `UnitOfWork.Discard()`   {#discard}

Discarding is the other half of the same decision, and until it existed the only way to abandon edits was to close
the thing holding them:

```osy syntax
action StartOver() {
  UnitOfWork.Discard();     // the page's pending edits are gone; the page itself stays open
}
```

It clears **one** unit of work — the one you are in — and stops there. That asymmetry with `Commit()` is deliberate
and it matters: a commit reaches outward, because persisting is the outermost unit of work's job and an inner scope's
edits have to get there. A discard must not, or closing an inner surface would take the surrounding page's unsaved
work with it.

Reads fall back to what the server last confirmed, so a field that was edited shows its stored value again, and a row
created only in the overlay is simply no longer there.

### Where a create-form's draft belongs   {#draft-scope}

A draft is an ordinary pending row, so it obeys the rule above: **the page's own queries read it back**. That is the
feature — it is what makes `Add` show the new item instantly — and it is also the one way a create form goes wrong.

A component that holds `Entity draft = new Entity { };` **and** queries that same entity has enlisted a row before
anybody has typed. Two things follow, neither visible in the source: the list paints a **blank phantom row** on first
paint, and the never-filled draft rides the next genuine `Commit()`, where its unset `[Required]` fields fail the
save and name a row the user never opened. `osy lint` reports the pair as `ui-draft-field-ghosts-its-own-list`
(SHOULD) — it fires on the member-initialiser spelling and on `on mount { draft = new Entity { }; }` alike, because
both run at mount.

Two ways out, and they are not equivalent:

- **Give the draft its own unit of work** — put it on a component you open with
  `Dialog.Open(NewThing(), unitOfWork: Root)` ([Dialog.Open / Dialog.Ask / Dialog.Confirm / Dialog.Discard](https://osysharp.com/reference/ui/dialogs/)). Nothing is pending in the list's unit of work, and
  the draft stays an entity, so every `[Required("…")]` sentence is still declared once on the model
  ([Validation](https://osysharp.com/reference/ui/validation/)). **This is the one to reach for.**
- **Hold the fields as local scalars** (`string title = "";`) and construct the entity inside the action that saves
  it. Nothing is pending until the action runs — but the fields are no longer entity properties, so a bound control
  has no rules to configure itself from and every declared message has to be re-typed as a guard in the action. Right
  when the form does not correspond to one entity; a real cost otherwise.

⚠ The **kit `Dialog(title, onDismiss)` control is not the first option.** It is `[Composable]` markup inlined into
the page's own tree and carries no unit of work at all, so a draft inside it is pending in the page's — exactly the
shape being fixed.

## Examples       {#examples}
A list the user builds up before saving — `Add` creates a row optimistically (it appears in the list at once, via
`foreach` reading through the overlay); a separate **Save** action is the only place that commits:

```osy title="item-list" test app=ui-items
entity Item { string Title; }

[Page("/items")]
[Render(CSR)]
component ItemList() {
  string draft = "";
  var items = Item.ToList();

  action Add()  { new Item { Title = draft }; }   // optimistic — shows immediately, not yet saved
  action Save() { UnitOfWork.Commit(); }          // the user's Save button — persists the whole batch

  render {
    Stack(gap: 2) {
      foreach (var it in items) { Text(it.Title); }
      Input(value: draft, placeholder: "New item");
      Button("Add", onPress: Add);
      Button("Save", onPress: Save);
    }
  }
}
```

The simplest form — create and Save in one action — is just the same pattern with `new` and `UnitOfWork.Commit()` in a single
Save handler:

```osy title="note-form" test app=ui-notes
entity Note { string Title; }

[Page("/notes/new")]
[Render(CSR)]
component NoteForm() {
  string title = "";

  action Save() {
    new Note { Title = title };
    UnitOfWork.Commit();
  }

  render {
    Stack(gap: 2) {
      Input(value: title, placeholder: "Note title");
      Button("Save", onPress: Save);
    }
  }
}
```

## Editing a row — bind an input to its field   {#edit-binding}
An **edit form** binds an input straight to a **field of a loaded row**: `Input(value: org.Name)`. This is a **two-way
binding**, exactly like binding to a client field — but the target is an entity field, so:

- the input **pre-fills** with the row's current value, and
- typing writes the change **into the page's overlay** (not the database) — the same optimistic overlay a `new`
  accrues into. The page becomes **dirty** as the user types (its tab shows the unsaved-work marker), and the edit is
  persisted only when a **Save** action calls `UnitOfWork.Commit()` — or discarded if the user closes the tab.

The row itself comes from a **scalar server read** (`Single`/`FirstOrDefault`), typically keyed by a route parameter, so
the page loads exactly the record being edited. There are three binding targets and no others: a client-field scalar
(`Input(value: draft)`), a loaded entity field (`Input(value: org.Name)`), and a `Binding<T>` prop this component was
handed ([generic component](https://osysharp.com/reference/ui/generic-component/)).

⛔ **A field of a `class` value is NOT one of them, and this is a data-model decision.** A `class` is an in-memory
shape with no row behind it, so there is nothing to write through — the compiler refuses it rather than rendering a
box that quietly discards what is typed: *"cannot two-way bind to `k.Weight` — `k` is a `class` … the edit would be
read-only."* A `live var` is refused for the same reason: it is computed, so *"there is nothing to write back into."*
**So if a page must let a person EDIT the rows of a list, those rows are an `entity`** — hold the value in a
component field and copy it into the class when you save, or make the row a real entity. Deciding that up front is
much cheaper than discovering it once the page is written; the full target list is at [[ui-component#two-way]].

```osy title="editing a loaded row" test app=ui-org-edit
[Principal] entity User { [Required] string Email; }

entity Organization {
  [Required] string Name;
  [Required] string Slug;
  security { allow create, read, update when IsAuthenticated; }
}

[Page("/org/{id}")]
[Render(CSR)]
component OrgEdit(Guid id) {
  var org = Organization.Single(o => o.Id == id);   // load the one row (route param → scalar query)

  action Save() { UnitOfWork.Commit(); }                          // persist the edits the bindings accrued

  render {
    Stack(gap: 2) {
      Input(value: org.Name);                          // pre-fills; typing dirties the page overlay
      Input(value: org.Slug);
      Button("Save", onPress: Save);
    }
  }
}
```

For the row to be editable in the overlay it must be **held in the page's own unit of work** — a scalar server read on the
page does exactly that. (An input bound to a row created on a *different* page/tab would be writing into the wrong
overlay; each retained tab has its own.)

## Calling a server function   {#server-calls}
An action can call a **server function** mid-flow — to run logic that belongs on the server (a privileged read, a
cross-record calculation). You just call it like any other function; the hand-off happens under the hood (no `await`,
no ceremony). The boundary is seamless in both directions:

- The server function **sees your pending edits** — the page's uncommitted overlay travels with the call, so the
  server reads the same in-progress data the user is looking at.
- Whatever the server function **creates or changes comes back**, and the rest of your action reads it immediately —
  *read-your-writes* across the boundary.

⚠ **From an `[AllowAnonymous]` page, the server function must be `[AllowAnonymous]` too.** A signed-out visitor may
only hand off to a target that says it is public, so a plain server function called from a public page is
refused — and the refusal arrives *at the call site*, which unwinds the rest of the action: the write does not
happen and **no statement after the call runs either**. Marking the function `[AllowAnonymous]` does not open its
data: every read and write inside it is still gated by the entity's own `security { }`. This compiles clean today,
so it is the one part of the hand-off the compiler cannot yet warn you about.

Crucially, this **keeps the same Save agency**: rows the server produced ride back into the page overlay **still
uncommitted** — they show up in the UI, but the user's **Save** is what persists them, exactly like a local `new`.
The one exception is deliberate: if the server function itself calls `UnitOfWork.Commit()`, its own writes persist server-side
at that point (server logic can own its transaction). So "nothing persists until Save" holds for ordinary server
calls, and a server function only bypasses it by explicitly committing — which the compiler flags with a warning so
it's never a silent surprise.

```osy syntax
action Reserve() {
  var seat = AssignSeat(row);      // server picks a seat, returns the row it created (hand-off is implicit)
  note = "You got " + seat.Label;  // read-your-writes: the returned row is visible here
}                                  // …still uncommitted — the user's Save persists it,
                                   //    unless AssignSeat itself called UnitOfWork.Commit()
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the component an `action` and its bound state live in
- [layout primitives](https://osysharp.com/reference/ui/layout/) — the `Stack`/`Input`/`Button` atoms a form is built from
- [routes and pages](https://osysharp.com/reference/ui/routing/) — binding the form page to a route


---

<!-- https://osysharp.com/reference/ui/debounce/ -->

# debounce

> `debounce: 300` tells a control to wait for a pause before it runs its event handler. Without it, an `onInput` handler fires on every keystroke — so a handler that asks the server a question would make one round-trip per character. With it, you get one, when the user stops typing.

<!-- id: ui-debounce · area: ui · stability: stable · html: https://osysharp.com/reference/ui/debounce/ -->

## Summary        {#summary}
`debounce:` is a property of the control, written on the call. It coalesces that control's event handlers: the handler
runs once, after the events stop for the given number of milliseconds.

```osy syntax
Input(value: draft.Slug, onInput: CheckSlug, debounce: 300);
```

Type `acme` quickly and `CheckSlug` runs **once**, 300ms after the last keystroke — not four times.

## Signature      {#signature}
```osy syntax
debounce: <milliseconds>      // a non-negative whole number; applies to this element's event handlers
```

- It needs an event handler to coalesce (`onInput`, `onClick`, …). A `debounce:` with no handler is a compile error —
  it would do nothing, silently.
- The handler runs with the **latest** value: the field's binding is written before the handler runs, so a handler
  that reads the field it is bound to sees what the user just typed.

## Description    {#description}
Some handlers are cheap and want to fire on every event. Some ask a question that costs a round-trip — *is this name
available?*, *what matches this search?* — and firing those per keystroke is wasteful and racy. `debounce:` is how you
say "wait until they stop."

**What debounce does not do.** It does not deduplicate answers. If a handler asks the server something, a slow reply
for an earlier value can still land after a faster reply for a later one. When the answer is written into state, guard
it: re-read the field after the call and drop the answer if it no longer describes what's in the box (the example
below does this). Debounce reduces the number of questions; it does not order the answers.

**It is not validation.** A friendly as-you-type check is a courtesy. The rules that actually protect your data are the
ones declared on the entity — `[Unique]`, `[Required]`, `[Pattern]` — and those are enforced when the data is saved,
whatever the client did or didn't check first.

## Examples       {#examples}
The is-this-name-taken check. An ordinary server function answers the question; an ordinary action asks it and writes
the answer into state; `debounce:` makes it one round-trip per pause in typing:

```osy title="slug-availability" test app=ui-debounce
entity Organization {
  [Required, MaxLength(100), Unique, Pattern("^[a-z0-9]+(-[a-z0-9]+)*$")] string Slug;
  string Name;
}

// An ordinary server function — it just answers a question.
bool CheckSlugAvailability(string candidate) {
  if (candidate == "") { return true; }
  return !Organization.Any(o => o.Slug == candidate);
}

[Page("/org/new")]
[Render(CSR)]
component OrgCreatePage() {
  Organization draft;
  bool slugFree = true;
  on mount { draft = new Organization {}; }

  // An action may write state and may call a server function. Re-reading the field after the call drops a stale
  // answer — the reply for a slug the user has already typed past.
  action CheckSlug() {
    var candidate = draft.Slug;
    var free = CheckSlugAvailability(candidate);
    if (candidate == draft.Slug) { slugFree = free; }
  }

  render {
    Stack(gap: 4) {
      Input(value: draft.Name, placeholder: "Organization name");
      Input(value: draft.Slug, placeholder: "team-slug", onInput: CheckSlug, debounce: 300);
      if (!slugFree) { Text("That slug is taken."); }
    }
  }
}
```

A search box wants the same thing — one query per pause, not one per letter:

```osy title="a search box — one query per pause, not one per letter" syntax
Input(value: term, onInput: Search, debounce: 250);
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — `action` members, and why an action (not an `on change` block) is what asks a question like this.
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the controls an event handler can hang off, and their event props.
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — binding an input to an entity field, and `UnitOfWork.Commit()`.


---

<!-- https://osysharp.com/reference/ui/drag/ -->

# drag

> `drag:` binds a number to a drag gesture: grabbing the element moves the value, arrow keys move the same value, and Escape puts it back where it started. You write no drag code — no begin handler, no cancel handler, no keyboard handler — which is what makes those three agree.

<!-- id: ui-drag · area: ui · stability: stable · html: https://osysharp.com/reference/ui/drag/ -->

## Summary        {#summary}
A resize handle, a splitter, a slider — anything where a gesture moves a **number** — is one prop:

```osy syntax
component Splitter() {
  int width = 240;
  render {
    Row {
      Box(w: width) { Text("sidebar"); }
      Box(drag: width, dragAxis: DragAxis.Horizontal, dragStep: 8, dragMin: 160, dragMax: 480) { Text("⋮"); }
      Box { Text("content"); }
    }
  }
}
```

That is the whole feature. Dragging the handle writes `width`; <kbd>←</kbd>/<kbd>→</kbd> write the same `width` in
steps of 8; <kbd>Home</kbd>/<kbd>End</kbd> go to 160 and 480; <kbd>Esc</kbd> mid-drag puts it back where the drag
started. None of that is code you write.

## Signature      {#signature}
```osy syntax
drag:     <a numeric state member>     // the value the gesture moves — two-way
dragAxis: DragAxis.Horizontal | DragAxis.Vertical        // REQUIRED
dragStep: <number>                     // how far one arrow key moves it (default 1)
dragMin:  <number>                     // clamp, and where Home goes
dragMax:  <number>                     // clamp, and where End goes
```

## Description    {#description}

### It binds a VALUE, and that is why the hard parts are already right   {#binding}
The obvious shape for a drag is an event — "call me with the delta". This is a **binding** instead, and the
difference is not stylistic. Two things follow from the platform owning the value rather than the app:

**Escape restores.** The pre-drag value is snapshotted when you grab the handle, and Escape writes it back. With an
event you would hold that start value yourself, and whether it is still right depends on when your begin handler ran,
whether a re-render reset it, and what happens when two drags interleave. Cancel is the part of a drag that is always
skipped and always missed; here there is nothing to skip.

**The keyboard works.** Arrow keys write the *same bound value* through the *same code path*. There is no second
handler to write and therefore no second handler to forget, get subtly wrong, or leave behind when the drag logic
changes.

You also get, without asking: the drag surviving the pointer leaving the element (so a fast drag does not drop), a
movement threshold (so a click on the handle does not nudge the value), and the right touch behaviour on a phone.

### `dragAxis` is required, and that is deliberate   {#axis}
It is the one required option in the UI vocabulary. The axis decides three separate things — which direction of
movement is read, which arrow keys respond, and **the element's touch behaviour**.

That last one is why there is no default. On a touchscreen a drag and a scroll begin identically, so the element has
to declare which it is *before* the finger moves. A horizontal drag leaves vertical panning to the page, so the page
still scrolls over your control. Get it wrong and everything looks perfect on a desktop and the page will not scroll
on a phone — so the platform asks rather than guesses.

### The handle is focusable   {#keyboard}
Declaring `drag:` makes the element focusable, because a keyboard path you cannot reach is not a keyboard path. Click
it or <kbd>Tab</kbd> to it, then use the arrows.

### What it does not do   {#not-drag-and-drop}
`drag:` moves a **number**. Dragging an *object* — a card onto a lane, a row to a new position — is drag-and-drop,
which needs to know what you picked up and what you dropped it on; that is a different vocabulary and it is not this
prop with a different type.

There is also no `Both` axis. A `drag:` binds one number, and a two-axis gesture has no single value to write into
it.

## Examples       {#examples}

```osy title="a draggable splitter, keyboard included" test app=ui-drag
component Splitter() {
  int width = 240;
  render {
    Row {
      Box(drag: width, dragAxis: DragAxis.Horizontal, dragStep: 8, dragMin: 160, dragMax: 480) {
        Text("drag me");
      }
      Text(width);
    }
  }
}
```

```osy title="a vertical drawer height" test app=ui-drag-vertical
component Drawer() {
  int height = 120;
  render {
    Box(drag: height, dragAxis: DragAxis.Vertical, dragStep: 16, dragMin: 40) {
      Text(height);
    }
  }
}
```

## See also       {#see-also}
- [keys](https://osysharp.com/reference/ui/keys/) — declaring a key surface, and reading whether a key is held right now
- [onEscape](https://osysharp.com/reference/ui/on-escape/) — Escape as dismissal elsewhere; a drag consumes its own Escape while one is in flight
- [component](https://osysharp.com/reference/ui/component/) — state, actions, and the render block these examples are written in
- [layout primitives](https://osysharp.com/reference/ui/layout/) — `Row`/`Stack`/`Box`, the elements a `drag:` goes on


---

<!-- https://osysharp.com/reference/ui/generic-component/ -->

# generic component

> A component with type parameters, bound at each call site from the arguments — one component that works over any enum or any row type, instead of a near-identical copy per type.

<!-- id: ui-generic-component · area: ui · stability: preview · html: https://osysharp.com/reference/ui/generic-component/ -->

## Summary        {#summary}
A `component` may declare type parameters. They are **inferred at each call site** from the arguments — there is no
constraint to declare and no type to pass — so one `Dropdown<TEnum>` serves every enum in the app, and one
`Table<TRow>` serves every row type. Inside the body a type parameter is a real type: it types parameters, action
parameters and locals, and `TEnum.Members` reads the members of whichever enum the call site bound.

## Signature      {#signature}
```osy syntax
component Name<T>(Binding<T> value) { … }        // T inferred from the bound field's type
component Name<T>(T[] rows) { … }                // T inferred from the collection's element type
component Name<T>(T item) { … }                  // T inferred from the argument's type

component Name<T>(Binding<T> value) where T : enum { … }   // …or state the constraint outright

Name(value: org.Status)                          // the call site says what T is
```

## Description    {#description}

### The type parameter is inferred at the call site   {#inference}
A call site already names the type: `Dropdown(value: org.Status)` can only mean `OrganizationStatus`. So there is no
explicit type argument to pass. A parameter binds a type parameter when it is declared as `T` (from the argument's own
type), `T[]` (from the collection's **element** type) or `Binding<T>` (from the bound field's type). The first
parameter that names a given type parameter wins; a later one is checked against that binding.

**What the type parameter must BE** is normally inferred too, from what the body does with it: a body that reads
`T.Members` has said that `T` is an enum, and every call site is checked against that. You do not have to write it
down — see *Declaring the constraint* below for the one case where you do.

### Declaring the constraint — `where T : enum`   {#constraints}
Inference works because the compiler can see the body. **A component you did not declare in this app — one from a
kit or capability — is compiled separately, and its body is not re-read here.** So there is nothing to infer from,
and a call to it is refused rather than guessed at:

> `'Dropdown' is generic, but nothing here says what 'TEnum' has to be…`

Guessing "unconstrained" would be worse than refusing: the body would ask for the members of something that is not
an enum, get none, and render an empty control that looks live and does nothing.

A **declared** constraint fixes this, because it lives on the declaration rather than in the body, and the
declaration is what crosses into your app:

```osy title="a generic control a kit can ship" syntax
component Dropdown<TEnum>(Binding<TEnum> value) where TEnum : enum {
  render {
    foreach (var m in TEnum.Members) { Text(m.Label); }
  }
}
```

`enum` is the only constraint kind. Write `where` once per constrained parameter, after the parameter list:
`component Pair<A, B>(A a, B b) where A : enum where B : enum`.

**Rule of thumb:** if the component lives in your own app, you never need `where` — inference covers it. Declare it
when you ship a component for other apps to call and they cannot see its body.

⚠ **A declared constraint is unconditional**, which is exactly what you want for a component that is always over an
enum — and exactly what you must not write for one that is only *sometimes* over an enum. See the next section.

### A constraint that only applies when you fall back   {#fallback-constraints}
A **fallback** — a parameter's default value, or a slot's default body — is what the component does *instead of*
something you may supply. So a demand it raises binds only the call sites that actually reach it:

```osy syntax
component Dropdown<T>(Binding<T> value, T[] options = T.Members) {
  render {
    foreach (var o in options) { Slot(o) { Text(o.Label); } }
  }
}

Dropdown(value: order.Status)                                       // T = OrderStatus — falls back, so T must be an enum
Dropdown(value: project.Lead, options: people) { p => Text(p.Name); }   // T = User — supplies both, so it need not be
```

Both defaults above say the same thing: *if you name no options, and no template, this is an enum picker*. Neither
says *T is always an enum* — and reading them that way would refuse the second call, over a constraint neither of its
arguments touches.

The two are suppressed by different things, so they narrow separately:

| Fallback | Applies to a call site that… |
|---|---|
| `T[] options = T.Members` | passes no `options:` argument |
| `Slot(o) { … }`'s default body | writes no template for that slot |

Anything the body does **outside** a fallback still binds every call site, however much it supplies. And a slot whose
name is computed (`Slot(col.Key, row)`) cannot be matched against a call site's fills, so a demand from its default
body stays unconditional — an escaped demand would render the empty, live-looking control this check exists to refuse.

⚠ **This is why the kit's `Dropdown` declares no `where` clause.** It is one component over an enum *or* over any
rows, and its enum-ness lives entirely in those two fallbacks; a declared `where T : enum` would re-refuse every
collection-backed call.

### What a type parameter can do inside the body   {#in-the-body}
It behaves as a type, not as an escape hatch — the body is fully checked:

| Written | Means |
|---|---|
| `Binding<T> value` | a two-way binding over a `T`, exactly like `Binding<OrgRole>` |
| `action Choose(T m)` | an action taking a `T` |
| `value = m` | assignment, when both are the same `T` |
| `m == value` | equality, when both are the same `T` (two *different* type parameters are not comparable) |
| `T.Members` | every member of the enum `T` was bound to (see below) |
| `m.Label` / `m.Description` / `m.Name` | the enum's words ([[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/)), read off a `T`-typed value |
| `Icon(m)` | the glyph the member declares with `[Icon(…)]` |
| passing `m` where a tone enum is expected | the tone the member declares with `[Tone(…)]` |

Anything else off the bare name — `T.Anything` — is a compile error: a type parameter has no members of its own.

### `T.Members` and when it is an enum   {#enum-constraint}
Reading `T.Members` (or a word off a `T`-typed value) is what makes `T` an **enum** — that use IS the constraint. Every
call site is then checked, and binding something that is not an enum is a compile error naming the component, the
parameter and the type it got:

> `'Dropdown' reads `TEnum.Members`, so 'TEnum' has to be an ENUM — but 'value' here is string.`

A type parameter the body never uses as an enum — `Table<TRow>(TRow[] rows)` — binds to anything.

### Decoration comes from the model, not from the component   {#decoration}
`Icon(m)` renders the `[Icon(…)]` the member declares, and passing `m` where a tone enum is expected renders its
`[Tone(…)]` — the same two spellings that already work on a concrete enum value, now reaching a type parameter. So a
generic component decorates every enum without naming a single member.

Both carry a completeness demand to the call site, because a half-decorated enum would render a **blank glyph** or an
unstyled row — something that looks deliberate and carries none of the meaning the model declared. Binding an enum
whose members do not all declare what the body renders is a compile error naming the members that don't:

> `'Dropdown' renders each member's [Icon(…)], so EVERY 'Bare' member needs one — 'NoIcon' does not.`

This is the same check the concrete spelling makes; it simply moves to the call site, which is the only place that
knows which enum is in play.

The list is the same one `<Enum>.Members` gives for a concrete enum, in declaration order, and the values are the same,
so `m == value` and `m.Label` behave identically either way. The difference is **when**: a concrete `Status.Members` is
expanded by the compiler, while `T.Members` is read at render time from the enum this particular instance was bound to.
That is what lets one component body serve two call sites over different enums on the same page.

### One body, many call sites   {#one-body}
A generic component is compiled **once**. The type each call site inferred travels to that instance, so two instances
of one component can list two different enums side by side. Nothing is duplicated per type: adding a fifth enum-backed
picker to an app adds a call, not a component.

## Examples       {#examples}

One dropdown, two enums, on one page. Both instances are the same component; each lists its own enum's members and
writes the chosen one back through its binding.

⚠ **The `Dropdown` below is THIS PAGE'S, declared above — not the kit's.** The name is deliberate (a generic control
is what the kit ships one of), and the shape is cut down to the one thing being taught: type inference. The kit's own
control takes a required caption first — `Dropdown("Status", value: order.Status)` — see [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/).

```osy title="one dropdown for every enum" test app=ui-generic-component
enum Tone { Neutral, Accent }

enum Status {
  /// Open for business
  [Tone(Tone.Accent)]  [Icon(check)] [Label("Active")] Active,
  /// Temporarily closed
  [Tone(Tone.Neutral)] [Icon(close)] [Label("Suspended")] Suspended,
}

enum Role {
  [Tone(Tone.Accent)]  [Icon(check)] [Label("Owner")] Owner,
  [Tone(Tone.Neutral)] [Icon(close)] [Label("Member")] Member,
}

// One option row. `tone` is an ordinary enum-typed param — passing a member of ANOTHER enum coerces to the tone that
// member declares, which is what lets the generic body above decorate without knowing the enum.
[Composable] component Option(string label, Tone tone) {
  // Literal colours here only because this example declares no `theme` — a real app uses its declared tokens.
  variants { base { Px = 1; } tone { Neutral { Bg = "#f6f6f7"; } Accent { Bg = "#e8f0fe"; } } }
  render { Row(gap: 1) { Slot; Text(label); } }
}

// `[Composable]` for the same reason `Option` above has it: the page below is PUBLIC, and an anonymous visitor can
// only load a component that says it carries no auth identity of its own ([[Composable] — presentational components in public pages](https://osysharp.com/reference/ui/composable/)). Its data reads, if it
// had any, would still be gated.
[Composable]
component Dropdown<TEnum>(Binding<TEnum> value) {
  bool open = false;
  action Toggle() { open = !open; }
  action Choose(TEnum m) { value = m; open = false; }

  render {
    Box(position: Position.Relative) {
      Pressable(onClick: Toggle) { Text(value.Label); }
      if (open) {
        Stack(gap: 0) {
          foreach (var m in TEnum.Members) {
            // Every option's glyph and tint comes from the member's own declaration — nothing here names one.
            Pressable(onClick: () => Choose(m)) { Option(m.Label, m) { Icon(m, size: 14); } }
          }
        }
      }
    }
  }
}

[Page("/settings")]
[AllowAnonymous]
component SettingsPage() {
  Status status = Status.Active;
  Role role = Role.Member;
  render {
    Stack(gap: 2) {
      Dropdown(value: status);
      Dropdown(value: role);
    }
  }
}
```

A type parameter that is never used as an enum takes any type — this is the row-type case, and it is checked the same
way (the datum in the template binds to `T`).

```osy title="a generic row list" syntax
component Table<TRow>(TRow[] rows) {
  render { Stack(gap: 1) { foreach (var r in rows) { Text("row"); } } }
}

Table(rows: expenses)     // TRow = Expense
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the component archetype these extend
- [[Label], [Icon], [Tone] — what a human reads](https://osysharp.com/reference/enum/labels/) — `[Label]` / `[Icon]` / `[Tone]`, and the `Label` / `Description` / `Name` words a member carries
- [enum](https://osysharp.com/reference/enum/declaration/) — declaring the enums a generic component is bound to


---

<!-- https://osysharp.com/reference/ui/icons/ -->

# icons

> Drop `.svg` files into `model/icons/` and render them with `Icon(Icons.Search)`. The name is checked at compile time, so a typo is an error rather than a blank square. An icon inherits the surrounding text size and color, so one icon set follows your theme through light and dark; pass `size:` when a glyph needs its own size. `Icons` is also a type, so an icon can be a parameter, a return value or a stored field.

<!-- id: ui-icons · area: ui · stability: stable · html: https://osysharp.com/reference/ui/icons/ -->

## Summary        {#summary}
Icons are **files in your app**, not data. Put an `.svg` in `model/icons/` and it becomes part of your app's
vocabulary:

```text
model/
  icons/
    search.svg
    close.svg
    menu.svg
```

```osy title="an icon beside a field" test app=file-manager
[Composable]
component SearchBar() {
  string query = "";
  render {
    Row(gap: 2) {
      Icon(Icons.Search);
      Input(value: query, placeholder: "Search");
    }
  }
}
```

This compiles against the [drop-ship-order](#see-also) sample, whose `icons/` folder really does contain
`search.svg` — which is the whole point: the name is checked against the files the app ships, so this example is
wrong the moment that file is renamed.

Adding an icon is dropping a file in. There is nothing to register and nothing to import.

## Signature      {#signature}
```osy syntax
Icon(Icons.Search) · Icon(Icons.Search, size: 18) — an icon, named by a compile-checked identifier
```

## Description    {#description}

### The name is checked   {#names}
`Icon(Icons.Search)` names the icon by a **bare identifier**, and it is checked against the icons your app actually
declares. A typo is a compile error with a suggestion:

```text
unknown icon 'serch' (declared icons: close, menu, search). Did you mean 'search'?
```

Because the name is an identifier, an icon's **file name must be one too** — `chevron_right.svg`, not
`chevron-right.svg`. A kebab-case file is rejected with the rename to make.

The name is never an expression. A local variable called `search` does **not** change what `Icon(Icons.Search)` means —
the icon vocabulary always wins. This is deliberate: an icon chosen at runtime could not be checked, so there is no
way to write one by accident.

**Choosing an icon from data** — a category's icon, say — is a *content* concern, not chrome. Use `Image(src)`, or
branch explicitly:

```osy syntax
if (item.Kind == Kind.Folder) { Icon(Icons.Folder); } else { Icon(Icons.File); }
```

### How big is an icon, and what color? — it inherits   {#styling}
An icon is an **em square that inherits the current text color**. Put one beside a label and it matches at any font
size, in light mode and dark, with nothing to configure:

```osy title="an em square that already matches the label beside it" syntax
Row(gap: 2) { Icon(Icons.Search); Text("Search"); }
```

**Color** defaults to the surrounding text color — any color you draw into the `.svg` is replaced by the current
text color when your app is built, so an icon copied from any icon set immediately follows your theme. Override one
glyph's color with `color:` — a theme token or a color string:

```osy title="overriding one glyph's color on the call, never a tint wrapper" syntax
Icon(Icons.Chev, color: Colors.TextMuted)        // a muted separator in primary-colored text
Icon(Icons.Close, size: 24, color: Colors.Accent)
```

Like `size:`, color is a property of the glyph written on the call — never a tint wrapper around it. An **outline**
icon (drawn as `fill="none"` plus a stroke) stays an outline.

**Size** defaults to the surrounding text size, and you override it per call with `size:` — a length:

```osy title="overriding one glyph's size on the call" syntax
Icon(Icons.Search, size: 18)       // 18px
Icon(Icons.Check, size: 14)        // 14px — a denser glyph
Icon(Icons.Menu, size: "1.5em")    // relative to the surrounding text
```

Size is a property of the glyph, so it is written on the call — never a wrapper component around it. If your app
uses a handful of standard sizes, name them with an enum and pass the member (its value is the length):

```osy title="naming your standard sizes" test app=file-manager
enum IconSize { Sm = 14, Md = 18, Lg = 24 }

[Composable]
component SizedSearch() {
  render { Icon(Icons.Search, size: IconSize.Md); }
}
```

`size:` is the only argument `Icon` takes besides the name; a bare second argument or any other named argument is a
compile error, so `Icon(Icons.Search, 18)` is corrected to `Icon(Icons.Search, size: 18)`.

### What an icon may contain   {#contents}
An icon is shapes: `path` `circle` `ellipse` `line` `polyline` `polygon` `rect` `g`, and a `viewBox` on the root
`<svg>`. Titles, descriptions and `id`/`class` attributes are dropped — they aren't needed.

Anything that could **run, load, or reference** something is a compile error, naming the file:

```text
'evil.svg' contains a <script> element. An icon may only contain shape elements
(circle, ellipse, g, line, path, polygon, polyline, rect); scripting, styling,
embedding and animation are not allowed.
```

That covers `<script>`, `<style>`, `<image>`, `<a>`, `<use>`, animation elements, any `on…` handler, and any
attribute that points somewhere (`fill="url(#x)"`, `xlink:href`, a `javascript:` link). An SVG is a place scripts
can hide, and your icons are placed directly into your app's pages — so the rule is an allow-list, and it is not
negotiable.

An icon with no drawable content left, or with no `viewBox`, is also an error rather than an invisible square.

### Icons from a UI kit   {#kits}
A kit's icons land in your app tree alongside your own, and are picked up the same way. Two files claiming the same
name is an error naming both, so an icon always resolves to exactly one file.

### Passing an icon around — `Icons` is a type   {#as-a-value}
`Icons` is not only a spelling for a call site: it is a **type**, so an icon is a value you can pass, return, store
and compare like any other.

```osy title="a component that takes an icon" test app=drop-ship-order
[Composable]
component NavItem(string label, Icons icon) {
  render {
    Row(gap: 2) {
      Icon(icon);
      Text(label);
    }
  }
}
```

The caller names the glyph the same way it always did:

```osy syntax title="calling it — the call site does not change"
NavItem("Search", Icons.Search);
```

The same type works in every other position — a return type, a local, a parameter to an ordinary function, and a
field on an entity:

```osy syntax title="the same type in every other position — return, local, parameter, field"
entity NavEntry {
  string Label;
  Icons Glyph;                       // stored, keyed by the file's own name
}

Icons GlyphFor(bool searching) {
  return searching ? Icons.Search : Icons.Close;
}
```

Because a stored icon is keyed by the file's **name** and not by a position in a list, adding a new `.svg` to your
app never changes what an already-stored row means.

⭐ **A KIT CONTROL HOLDS THE SAME TYPE.** `Icons` is built from YOUR files, so it looks like a type only your own
app could name — but the compiler puts it in scope for the kits your app takes as well, and the vocabulary it hands
them is yours, merged with the built-ins. That is why `NavItem.Icon` in the bundled app shell is an `Icons`: your
`icons/logo.svg` is a nav row's glyph, a menu row's glyph and the shell's brand mark, written exactly as you write
`Icons.Search` anywhere else. There is one icon vocabulary in an Osy# app and a kit does not get its own.

### How icons are delivered — inline, on the first byte   {#delivery}
Your icons are combined into one small file, and it is **already in the page** when it arrives — a server-rendered
page paints its icons on the very first byte, before any script runs. There is no icon-flash, no request, and
nothing to configure. Change an icon and the file changes with it; leave them alone and browsers keep the copy they
already have.

Custom glob, if `model/icons/` doesn't suit you:

```osy syntax
app Admin {
  model "model/**/*.osy";
  icons "assets/icons/*.svg";
}
```

## See also   {#see-also}
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the tokens an icon's color resolves against.
- [layout primitives](https://osysharp.com/reference/ui/layout/) — `Row`/`Stack` and the `gap` that spaces an icon from its label.
- The **drop-ship-order** sample (`osy docs sample drop-ship-order`) — its `icons/` folder is the vocabulary the
  compiled examples above resolve against, and its storefront header uses one.


---

<!-- https://osysharp.com/reference/ui/keys/ -->

# keys

> `keys:` declares that an element owns a set of keys: it becomes focusable, those keys stop scrolling the page, and `Keyboard.Down(Left)` answers whether one is held right now. Held state is the primitive — `onKeyDown` is the discrete convenience over it — because "is Left down?" is the question a moving surface actually asks.

<!-- id: ui-keys · area: ui · stability: stable · html: https://osysharp.com/reference/ui/keys/ -->

## Summary        {#summary}
A surface that responds to the keyboard declares which keys it **owns**, and then reads them:

```osy syntax
component Board() {
  int x = 0;
  action Move(string key) { x = key == "Left" ? x - 1 : x + 1; }
  render {
    Box(keys: [Left, Right], onKeyDown: Move) {
      Text($"position {x}");
    }
  }
}
```

Declaring `keys:` does three things at once, and they are not separable — a surface that could not be focused would
hear nothing, and one that heard arrows without claiming them would scroll the page while it moved:

1. the element becomes **focusable**, so clicking it or tabbing to it gives it the keyboard;
2. it **owns** exactly the listed keys — they no longer scroll, and every other key is left alone;
3. `Keyboard.Down(...)` and `onKeyDown`/`onKeyUp` answer for those keys while it has focus.

## Signature      {#signature}
```osy syntax
// on any element — declares the surface
keys: [<Key>, <Key>, …]

// the discrete events (both optional; each hands the action the key NAME)
onKeyDown: <action>          // action Move(string key)
onKeyUp:   <action>

// the held-state read — legal in `render` AND in a client action body
Keyboard.Down(<Key>)   // bool: is that key down right now?
```

## Description    {#description}

### Held STATE is the primitive; the events are derived   {#held-state}
Two different surfaces ask two different questions about the keyboard, and only one ordering answers both:

| the surface | the question | what answers it |
|---|---|---|
| a grid, a list, a menu | *was an arrow **pressed**?* | `onKeyDown` |
| anything that **moves** | *is Left **down right now**?* | `Keyboard.Down(Left)` |

Holding an arrow slides a piece; holding W walks. No vocabulary of fired events can express that — you would end up
tracking "which keys are currently down" in your own state, updating it from two handlers, and getting the edge cases
wrong. So the **held set is the primitive**, and `onKeyDown` is the convenient discrete half over it.

This matters most for the case that looks like it works and does not: a held key **auto-repeats**, firing `keydown`
roughly thirty times a second. `onKeyDown` runs **once per press**, on the real transition — so an app that moves one
step per press gets one step. An app that wants continuous motion reads `Keyboard.Down` on each tick instead.

### Simultaneous keys just work   {#simultaneous}
Two reads are two independent reads:

```osy title="two held keys are two independent reads — no arbitration to lose" syntax
if (Keyboard.Down(W)) { walk(); }
if (Keyboard.Down(A)) { strafe(); }
```

Both are true while both keys are down. There is no "current key" and no arbitration to lose.

### Ownership is declared, never inferred   {#ownership}
Arrows and Space scroll the page. Naming a key in `keys:` is what makes the platform claim it — and claim **only** it.
Every key you did not list passes through untouched, so <kbd>Tab</kbd> still moves focus and the browser's own
shortcuts still work. That is the difference between a key surface and a page that has taken the keyboard hostage.

### It is focus-scoped   {#focus-scope}
A key surface hears keys while it **has focus**. Two boards on one page therefore never both move, and a keystroke
meant for a text field is not swallowed by a board elsewhere on the page.

This is the opposite scoping from [onEscape](https://osysharp.com/reference/ui/on-escape/), deliberately: Escape dismisses the thing that is *open*, which is
almost never the thing that is *focused*, so it listens page-wide. A key surface is the thing you are interacting
with. (When focus leaves mid-hold — a <kbd>Cmd</kbd>+<kbd>Tab</kbd> while holding an arrow — every held key is
released, because the browser delivers that key-up to whatever has focus now, which is not you.)

### Keys are NAMES, checked when you compile   {#names}
`Left`, `Space`, `W`, `Digit1`, `Shift` — written bare, from a fixed vocabulary. A misspelling is a compile error
that suggests the nearest real key, rather than a surface that renders, takes focus, and silently never responds.

Available: the arrows `Left` `Right` `Up` `Down` · `Space` `Enter` `Escape` `Tab` `Backspace` `Delete` ·
`Home` `End` `PageUp` `PageDown` · the letters `A`–`Z` · the digits `Digit0`–`Digit9` · the modifiers `Shift`
`Control` `Alt` `Meta`.

A modifier names **either** physical key — `Keyboard.Down(Shift)` is true for the left or the right one, and
`onKeyDown` still hands you `Shift`. You are never asked to care that there are two.

### Keys are PHYSICAL POSITIONS, not the character produced   {#physical}
`Keyboard.Down(W)` means *the key where W sits*, not *the key that types "w"*. This is what makes a `WASD` movement
cluster keep its shape when someone holds <kbd>Shift</kbd> to run, and on a keyboard layout that is not QWERTY.

The cost is worth stating plainly: on an AZERTY keyboard, `W` is the key physically where W is on QWERTY, whatever is
printed on the cap. That is what movement keys mean and what players expect — but it is why this is a **gesture**
vocabulary, not a text one. To read what somebody *typed*, bind an `Input` and use `onInput`, where keyboard layout
and IME are handled properly and this question never comes up.

### Where `Keyboard.Down` may be read   {#where-read}
In a `render` block, where the read is **reactive** — a surface bound to a held key repaints on press and on release
with nothing wired by you:

```osy title="read in render, so the surface repaints on press and on release" syntax
Box(keys: [Space], bg: Keyboard.Down(Space) ? Accent : Surface) { Text("hold me"); }
```

…and in a **client action body**, where it is a point-in-time read — which is what a tick handler wants.

It is not available on the server: no keyboard is attached to one. A server-rendered page paints as though nothing is
held (because nothing is), and the first real keystroke corrects it. A server function that reads it is a compile
error naming the conflict.

## Making a key surface focusable — `autoFocus`   {#auto-focus}

A key surface can take keys only while it **has focus** — `keys:` makes it focusable (`tabindex`), and clicking it
focuses it. `autoFocus: true` says it should start out holding focus, so the first keystroke works without a click
first.

```osy syntax
Box(keys: [Left, Right, Shift], autoFocus: true) { … }
```

⚠ **Without it, a modifier-gated click does the UNMODIFIED thing on a freshly loaded page** — and silently. If a
page reads `Keyboard.Down(Shift)` inside a click action to mean "flag rather than reveal", the very first
shift-click reveals instead, then works correctly ever after. That reads as a flake rather than as a missing
declaration, which is why the prop exists.

Focusing does **not** scroll: an `autoFocus` surface below the fold will not jump the page past the heading that
explains it. And it fires on the edge — when the value becomes true — never re-asserting on an unrelated re-render,
so it cannot yank focus back from wherever the reader has tabbed to.

## Examples       {#examples}

```osy title="a focusable board that moves on arrows" test app=ui-keys
component Board() {
  int x = 0;
  int y = 0;
  action Move(string key) {
    if (key == "Left")  { x = x - 1; }
    if (key == "Right") { x = x + 1; }
    if (key == "Up")    { y = y - 1; }
    if (key == "Down")  { y = y + 1; }
  }
  render {
    Box(keys: [Left, Right, Up, Down], onKeyDown: Move, p: 4) {
      Text($"({x}, {y})");
    }
  }
}
```

```osy title="held state — the surface reacts while the key is down" test app=ui-keys-held
component Thruster() {
  render {
    Box(keys: [Space], p: 4) {
      Text(Keyboard.Down(Space) ? "burning" : "idle");
    }
  }
}
```

```osy title="two keys at once — no arbitration, just two reads" test app=ui-keys-simul
component Strafe() {
  render {
    Box(keys: [W, A, S, D], p: 4) {
      Text(Keyboard.Down(W) && Keyboard.Down(A) ? "forward-left" : "idle");
    }
  }
}
```

## See also       {#see-also}
- [onEnter](https://osysharp.com/reference/ui/on-enter/) — Enter as the keyboard peer of a click, on a focused field
- [onEscape](https://osysharp.com/reference/ui/on-escape/) — Escape as dismissal, listening page-wide rather than on a focused element
- [component](https://osysharp.com/reference/ui/component/) — state, actions, and the render block these examples are written in
- [layout primitives](https://osysharp.com/reference/ui/layout/) — `Layout.AtLeast`, the other primitive that answers a question about the live page


---

<!-- https://osysharp.com/reference/ui/layout/ -->

# layout primitives

> The built-in layout primitives and how they arrange children. `Stack` stacks children in a column, `Row` lays them in a row, and `Box` is a plain container; `gap`, `align`, and `justify` control spacing and alignment.

<!-- id: ui-layout · area: ui · stability: stable · html: https://osysharp.com/reference/ui/layout/ -->

## Summary        {#summary}
Osy# ships a small set of **layout primitives** you compose your UI from:

| Primitive | Arranges its children |
|---|---|
| `Stack` | in a **column** (top to bottom) |
| `Row` | in a **row** (left to right) |
| `Box` | a single container with no intrinsic direction |

```osy title="stack, row, and the gap between" test app=ui-layout
[Composable]
component ProductCard(string name, string price) {
  action AddToCart() { }
  render {
    Stack(gap: 2) {
      Text(name);
      Row(justify: Justify.Between) {
        Text(price);
        Button("Add to cart", onPress: AddToCart);
      }
    }
  }
}
```

## Signature      {#signature}
```osy syntax
Stack / Row / Box — flexbox layout with gap, align, justify

Stack(stickToBottom: true)       { … }    // follow new content, but never fight the reader
Stack(stickToBottom: following)  { … }    // …and tell me when they scroll away
```

## Description    {#description}

### Spacing — `gap`   {#gap}
`gap` sets the space **between** a layout's children, as a step on the spacing scale (a whole number). Larger
numbers mean more space; `gap: 0` (the default) means no gap.

```osy syntax
Stack(gap: 4) { … }   // more space between rows
Row(gap: 1) { … }     // a little space between columns
```

### Alignment — `align` and `justify`   {#alignment}
`align` and `justify` are **built in** — they need no `using`, and no UI kit. The platform maps them straight to
flexbox, so a typo (`align: Align.Centre`) is a compile error rather than a silent no-op, and no kit can change what
`Align.Center` means.

**Write the value qualified — `Align.Center`, never a bare `Center`.** In an argument slot a bare capitalised name
could be a theme token, an enum member or a style keyword, and all three are spelled alike; the group name is what
says which vocabulary you meant. `align`'s group is `Align` and `justify`'s is `Justify`, so the value always reads
as *group*`.`*member*. A bare name there is refused, and the refusal names the spelling to write.

`align` positions children on the **cross axis**, `justify` distributes them along the **main axis** (the axis the
primitive lays out on — vertical for `Stack`, horizontal for `Row`).

| `align` | effect |
|---|---|
| `Align.Start` | pack to the start |
| `Align.Center` | center |
| `Align.End` | pack to the end |
| `Align.Stretch` | stretch to fill |
| `Align.Baseline` | align text baselines |

| `justify` | effect |
|---|---|
| `Justify.Start` / `Justify.Center` / `Justify.End` | pack to the start / center / end |
| `Justify.Between` | equal space between children |
| `Justify.Around` | equal space around each child |
| `Justify.Evenly` | equal space between and at the edges |

```osy syntax
Row(align: Align.Center, justify: Justify.Between) {
  Text("Title");
  Button("Action", onPress: Act);
}
```

These names are fixed (they map to the browser's flexbox model), so a typo like `align: Align.Centre` is a compile
error, not a silent no-op.

### Following new content — `stickToBottom`   {#stick-to-bottom}
A surface that grows while someone is reading it — a chat transcript, a log, a build console — should show the
newest content. But it must not yank a reader who has deliberately scrolled up to re-read something earlier. That is
the rule everybody gets wrong, and it is one word here:

```osy title="a transcript that follows" test app=ui-layout-stick
component Transcript(string[] Lines) {
  render {
    Stack(overflowY: Overflow.Auto, gap: 2, stickToBottom: true) {
      foreach (var line in Lines) { Text(line); }
    }
  }
}
```

It applies to a **scrolling** container — one with `overflowY: Overflow.Auto`. New content scrolls into view while the reader
is at the bottom; the moment they scroll up, following stops, and it resumes by itself when they scroll back down.

#### Knowing whether it is following, and jumping back   {#following}
Give it a `bool` field instead of a literal and the field becomes the container's *following* state, in **both**
directions. The container writes `false` into it when the reader scrolls away and `true` when they return — so your
app can show a *jump to latest* affordance — and setting it back to `true` yourself scrolls to the bottom and
resumes following:

```osy title="jump to latest" test app=ui-layout-stick
component Chat(string[] Lines) {
  bool following = true;

  action Jump() { following = true; }

  render {
    Box {
      Stack(overflowY: Overflow.Auto, gap: 2, stickToBottom: following) {
        foreach (var line in Lines) { Text(line); }
      }
      if (!following) {
        Button("Jump to latest", onPress: Jump);
      }
    }
  }
}
```

The button is yours to draw and place — the platform ships none. Setting the field is the only way to scroll a
container from Osy#, which is why the write half exists at all.

Note the difference between the two forms: `stickToBottom: <a bool expression>` is an on/off **switch** ("follow only
while the Live tab is open"), while `stickToBottom: <a field you can assign>` is the following **state**. With a
field, the behaviour stays on for as long as the container exists — a `false` means "not following right now", not
"switched off" — which is what lets the reader resume simply by scrolling back down.

## See also   {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — declaring a component and its `render` block.
- [[ui-component#render-tree]] — the full render vocabulary (text, conditionals, loops, bindings).
- [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) — rendering a message's text inside a transcript, including while it is still arriving.


---

<!-- https://osysharp.com/reference/ui/on-change/ -->

# on change

> `on change { … }` is a reactive **side-effect**: the runtime re-runs it whenever a value it read changes, so it's how you keep something OUTSIDE the component in sync with something inside it — most commonly the page's title (`on change { Navigation.SetTitle(org.Name); }`). It may not write the component's own state; that's a compile error.

<!-- id: ui-on-change · area: ui · stability: stable · html: https://osysharp.com/reference/ui/on-change/ -->

## Summary        {#summary}
`on change { … }` runs a block **reactively** — the runtime re-runs it whenever a value it reads changes. Use it to
push a value from the component to somewhere **outside** it. The canonical case is a page naming its own route:

```osy syntax
component OrgEdit(string slug) {
  var org = Organization.Single(o => o.Slug == slug);

  on change { Navigation.SetTitle(org.Name); }     // the tab + browser title track the org's name
}
```

It belongs to the **`on <event>`** lifecycle-event family — `on mount` (setup, once), `on change` (a tracked reaction),
`on unmount` (teardown, once). See [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) for the once-only siblings and [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) for the whole
execution model.

## Signature      {#signature}
```osy syntax
on change { <statements> }     // re-runs whenever a reactive value it read changes
```

`on change` takes **no name** — the `on <event>` family is anonymous. (Don't confuse it with an `onChange` **input
prop**: `Input(value: x, onChange: Handler)` is a field's change *event*, a different thing.)

## Description    {#description}

### What it's for   {#purpose}
An `on change` block is for **outward** work — a call whose result lands somewhere the component doesn't own:

| You want | Use |
|---|---|
| A computed **value** to render | `live var name = expr;` |
| Setup that runs **once**, when the page opens | `on mount { … }` ([on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/)) |
| Teardown that runs **once**, when the page closes | `on unmount { … }` ([on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/)) |
| To keep something **outside** the component in sync as data changes | `on change { … }` |

An `on change` body has the same powers as an `action` — it can call server functions and reach the client verbs
(`Navigation.*`, `Theme.*`). The runtime calls it for you instead of a click.

### It may not write its own state   {#no-self-write}
Assigning the component's own field from an `on change` block is a **compile error**:

> an `on change` block must not assign the component's own reactive state (`count`). That would loop: the write wakes
> the reaction, which re-runs it.

The rule holds even if the write hides behind an `action` or method the block calls — the compiler follows the call. If
you need a value, use a `live var` computed; if you need to set state once, use `on mount`; if a user gesture should set
it, use an `action`.

### When it runs   {#when}
It runs **at mount** (to establish its dependencies and do the initial sync), then **again whenever any reactive value
it read changes** — and only then. It is *dependency-tracked*: reading `org.Name` subscribes the block to `org.Name`,
so an unrelated change elsewhere on the page does not wake it. This is the whole point of the name — "on change" is
tracked-by-construction, where a block "that runs every render" would be waste. Triggers are **any** reactive read, not
only a `live var`: a plain state field reassigned by an action wakes it too. See [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/).

Writing a body that is **idempotent** — safe to run again with the same inputs — is the contract; the platform
de-duplicates at the sink where it can (calling `Navigation.SetTitle` with an unchanged title does nothing).

## Examples       {#examples}
A create page whose title tracks the name **as you type** it — and falls back while the name is still blank:

```osy title="reactive-title" test app=ui-on-change
entity Organization { string Name; }

[Page("/org/new")]
[Title("New organization")]          // the static fallback (server-rendered, and before the reaction first runs)
[Render(CSR)]
component OrgCreate() {
  Organization draft;
  on mount { draft = new Organization {}; }

  live var tabName = draft?.Name ?? "New organization";
  on change { Navigation.SetTitle(tabName); }    // the tab + browser title update as you type

  render {
    Stack(gap: 4) { Input(value: draft.Name, placeholder: "Organization name"); }
  }
}
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the component `on change` lives in, and its other members.
- [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) — `on mount` / `on unmount`, for setup and teardown that run once rather than reactively.
- [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) — the execution model: declarations vs `on mount` vs `on change` vs `on unmount`, and how a change
  updates only the slots that read it.
- [Navigation](https://osysharp.com/reference/ui/navigation/) — `Navigation.SetTitle`, and the rest of the route surface an `on change` block can reach.


---

<!-- https://osysharp.com/reference/ui/cadence/ -->

# on every

> `on every (TimeSpan.FromSeconds(5)) { … }` runs a block on a repeating cadence for as long as the component is mounted. Unlike `on frame` it is driven by the wall clock rather than the display, so it keeps running when the tab is hidden and costs nothing between ticks — it is the hook for a poll, a counter or any "do this again in N seconds". A component may declare as many as it likes, each with its own cadence.

<!-- id: ui-cadence · area: ui · stability: stable · html: https://osysharp.com/reference/ui/cadence/ -->

## Summary        {#summary}
`on every (<TimeSpan>) { … }` is a component **cadence hook**: the runtime runs the block over and over, on the
interval you name, for as long as the component is mounted. It belongs to the **`on <event>`** family alongside
[on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/)'s `on mount` / `on unmount` and the reactive `on change`.

It is an *auto-invoked action*, so it can do everything an action can — assign fields, create rows with
`new Entity{ … }`, and **call server functions**. That last one is most of the point: "refresh this from the server
every thirty seconds" is the canonical cadence.

```osy syntax
component Dashboard() {
  int seconds = 0;

  on every (TimeSpan.FromSeconds(1)) { seconds = seconds + 1; }

  render { Text($"up for {seconds}s"); }
}
```

## Signature      {#signature}
```osy syntax
on every (<TimeSpan>) { <statements> }                // the common form
on every (<TimeSpan>) (int elapsed) { <statements> }  // …plus how many intervals passed since the last run
```

The interval is a **`TimeSpan`**, never a bare number — `TimeSpan.FromSeconds(5)`,
`TimeSpan.FromMilliseconds(250)`, `TimeSpan.FromMinutes(1)`. A plain `500` is refused, because nothing in the source
would say whether it meant milliseconds or seconds and the two are a thousand-fold apart.

The hook is **anonymous**, and — uniquely — a component may declare **more than one**.

## Description    {#description}
The runtime arms a timer for the interval you named. When it fires it runs the block, and only once the block has
**returned** does it re-read the interval and arm the next one — so a body slower than its own cadence runs
back-to-back instead of stacking copies of itself, and a body that awaits a server call holds the clock while it
waits.

Because it is driven by the wall clock rather than the display, a hidden tab does not stop it: the browser may
throttle the timer, but time still passes and the missed intervals are reported to the next run (see
[missed ticks are coalesced](#elapsed)). That is the property that makes it right for a poll and wrong for an animation, and the
reverse of `on frame` — see [on every or on frame?](#versus).

### Several clocks in one component    {#several}
Each `on every` block is its own independent clock. This is the ordinary case, not an exotic one:

```osy title="a frame clock and two on-every clocks, side by side" syntax
component Monitor() {
  int frames = 0;
  int fps = 0;
  live var rows = Reading.ToList();

  on frame (double dt) { frames = frames + 1; }
  on every (TimeSpan.FromMilliseconds(500)) { fps = frames * 2; frames = 0; }   // a display counter
  on every (TimeSpan.FromSeconds(30)) { rows.Refresh(); }                       // a server poll

  render { Text($"{fps} fps"); }
}
```

A clock's identity is **where it is declared**, not the interval it happens to hold — so two blocks that name the same
interval are still two clocks, and changing an interval retimes the existing clock rather than creating a new one.

Here is a whole page that compiles — one counter on a one-second clock, a second clock at a different cadence, and a
[`on mount`](https://osysharp.com/reference/ui/lifecycle/) beside them, so the three hooks are seen coexisting rather than described:

```osy title="two clocks and a mount hook" test app=ui-cadence
[Page("/uptime")]
[Render(CSR)]
component UptimePage() {
  int seconds = 0;
  int minutes = 0;
  string label = "";

  on mount { label = "counting"; }

  on every (TimeSpan.FromSeconds(1)) { seconds = seconds + 1; }
  on every (TimeSpan.FromMinutes(1)) { minutes = minutes + 1; }

  render {
    Stack(gap: 2) {
      Text($"{label}: {seconds}s");
      Text($"{minutes} minute(s)");
    }
  }
}
```

### Can the interval depend on state?   {#computed}
The interval is an ordinary **expression**, re-read on each tick, so a cadence can depend on state:

```osy syntax
component Game() {
  int level = 1;

  // Every ten levels takes a slice off the interval — the piece falls faster as you play.
  on every (TimeSpan.FromSeconds(0.75 - (level - 1) * 0.06)) { StepDown(); }
}
```

A change takes effect at the **next** tick: the interval already in flight runs to completion, then the clock re-reads
and re-arms. Nothing cancels a pending tick early.

### What happens to a tick that was missed?   {#elapsed}
A page that could not keep up — a stalled tab, a body slower than its own interval — does **not** accumulate a queue of
runs. The missed intervals are **coalesced into the next run**, which is told how many there were:

```osy syntax
on every (TimeSpan.FromSeconds(1)) (int elapsed) {
  clock = clock + elapsed;      // count the time that really passed, not the ticks that really fired
}
```

`elapsed` is `1` on an ordinary tick and higher only when the page fell behind. This is why there is no "what happens
on overrun?" setting: **ignoring `elapsed` skips the backlog, and looping over it catches up** — the choice is a line
of your code rather than a mode. It is the same shape `Schedule` uses for a missed server job, which records one
occurrence carrying the number it absorbed.

The next tick is armed only after the body **returns** — including a body that awaited a server call — so a slow body
runs back-to-back rather than stacking copies of itself.

## `on every` or `on frame`?    {#versus}
Both repeat; they are for different things and neither substitutes for the other.

| | `on frame (double dt)` | `on every (TimeSpan)` |
|---|---|---|
| driven by | the **display** (~60 times a second) | the **wall clock**, on your interval |
| hidden tab | **pauses** — the display is not drawing | **keeps running** (the browser may throttle it) |
| can reach the server | no | **yes** |
| how many per component | one | as many as you like |
| for | animation, physics, anything per-frame | polls, counters, "again in N seconds" |

Choose `on frame` when the answer to "how often?" is *"every time the screen updates"*. Choose `on every` for
everything else — a five-minute poll on the frame clock would wake sixty times a second to do nothing 17,999 times out
of 18,000, and a poll on the frame clock silently stops the moment the reader switches tabs.

## Examples       {#examples}

The canonical cadence — refresh from the server on an interval, and a second clock at a different rate on the same
component, which is the part that has no equivalent in `on frame`:

```osy test app=ui-cadence
entity Reading { int Value; }

[AllowAnonymous]
int Latest() { return Reading.OrderByDescending(r => r.Value).Select(r => r.Value).FirstOrDefault(); }

[Page("/dashboard")]
[AllowAnonymous]
component Dashboard() {
  int latest = 0;
  int seconds = 0;

  // Reaches the SERVER — the whole point of the wall-clock hook.
  on every (TimeSpan.FromSeconds(30)) { latest = Latest(); }

  // A second clock, its own cadence. `elapsed` is 1 on an ordinary tick and higher only if the page fell behind,
  // so counting it rather than counting ticks keeps the number honest across a stalled tab.
  on every (TimeSpan.FromSeconds(1)) (int elapsed) { seconds = seconds + elapsed; }

  render {
    Stack(gap: 2) {
      Text($"latest {latest}");
      Text($"up {seconds}s");
    }
  }
}
```

## Limits — the clock runs on the client   {#notes}
- The hook runs on the **client**. A cadence that must survive the page being closed is a server concern — declare a
  `Schedule` instead, which is durable and operator-editable.
- A body that **throws** stops that clock and logs the error. Only that one: a broken counter does not take the poll
  beside it down.
- The clock is disposed with the component, so navigating away stops it.

## See also       {#see-also}
- [on mount / on unmount](https://osysharp.com/reference/ui/lifecycle/) — `on mount`, `on unmount` and the per-frame `on frame`
- [on change](https://osysharp.com/reference/ui/on-change/) — the *reactive* sibling: run a block when a value changes, rather than when time passes
- [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) — how a field a cadence writes reaches the screen


---

<!-- https://osysharp.com/reference/ui/lifecycle/ -->

# on mount / on unmount

> `on mount { … }` runs a block ONCE, the first time a component appears — before its first paint; `on unmount { … }` runs a block ONCE when it goes away — after its children tear down. Both have the full power of an action (set state, create data with `new Entity{}`, call a server function): `on mount` sets a page up, `on unmount` tears it down.

<!-- id: ui-lifecycle · area: ui · stability: stable · html: https://osysharp.com/reference/ui/lifecycle/ -->

## Summary        {#summary}
`on mount { … }` and `on unmount { … }` are component **lifecycle hooks** — the bookends of an instance's life. Each is
an *auto-invoked action*: the runtime calls it for you instead of a click, so each can do everything an action can —
assign a field, create rows with `new Entity{ … }`, call server functions. They belong to the **`on <event>`** family
alongside the reactive [on change](https://osysharp.com/reference/ui/on-change/) block.

- **`on mount`** runs **once, when the component first mounts**, *before its first render*. Its effects are in place
  for the first paint.
- **`on unmount`** runs **once, when the component goes away**, *after its children have torn down*. Use it for a final
  flush or an explicit release.
- **`on frame (double dt)`** runs **once per displayed frame**, for as long as the component is mounted. It is the
  odd one out and the only phase that repeats — see [running code every frame](#frame).

```osy syntax
component OrgCreatePage() {
  Organization draft;                          // no initializer — null until mount
  on mount { draft = new Organization {}; }    // seed a fresh draft to bind the form to
  on unmount { Log.Information("editor closed"); }    // a parting side-effect
  render { Input(value: draft.Name); … }
}
```

## Signature      {#signature}
```osy syntax
on mount   { <statements> }   // runs once, at first mount, before the first render
on unmount { <statements> }   // runs once, at teardown, after children tear down
on frame (double dt) { … }    // runs once per displayed frame; `dt` is the seconds since the last one
T name;                       // a field with NO initializer defaults to null (a bare C# field)
```

Both are **anonymous** (the `on <event>` family carries no name), and a component may declare at most one of each.

## Description    {#description}
Use `on mount` for the setup a page needs the moment it opens, and `on unmount` for the teardown it owes when it leaves:

- **Runs once, per instance.** `on mount` fires when a component instance first mounts; `on unmount` fires when that
  same instance is disposed. Switching to a retained tab and back is the *same* instance, so neither re-fires; a reload
  or reopening the page is a *new* instance, which mounts (and later unmounts) again.
- **`on mount` is before the first paint.** State it sets and rows it creates are reflected in the first render — no
  flash of an empty form.
- **`on unmount` is after the children.** Teardown runs deepest-first: a child region's `on unmount` fires before its
  parent's, and a page's own live queries and reactions are disposed automatically as its scope goes away — you never
  hand-unsubscribe. `on unmount` is for the *extra* teardown only your code knows about.
- **Full action powers.** Read/write a field, `new Entity{ … }` (into the page's overlay), call a server function. It's
  the same body a user action runs.

A companion: a field's **initializer is optional** (D64). `Organization draft;` declares the field with no value (null),
exactly like a C# field — so `on mount` (or an action) can fill it in later. This is what lets an edit form and a create
form look the same: the edit page *loads* its row into a server read, the create page *makes* one in `on mount`, and
both bind their inputs to that one entity.

**Creating vs. editing — the same shape.** Because a created row lives in the page's overlay just like an edited one, a
create page is dirty-tracked exactly like an edit page: a freshly-seeded, untouched draft is **not** dirty (closing the
tab discards nothing and prompts nothing), and it becomes dirty the moment you type. `UnitOfWork.Commit()` on a Save action
persists the draft.

**What these are not (yet).** The lifecycle family is `on mount` / `on unmount` / `on frame`; `on show`/`activate` and
`on route-change` are not built yet. Both run **client-side** (a server-rendered page paints without `on mount`, then
runs it on hydration). A block that reads a server-read field which hasn't loaded yet sees the loading state, the same
as an action would. These are known limits, not bugs.

### Running code every frame — `on frame (double dt)`   {#frame}
`on frame (double dt)` runs **once per displayed frame** — roughly sixty times a second — and is the only lifecycle
phase that repeats and the only one that takes a parameter. It is what a game loop, a simulation or a physics step
is written in, and it is the clock a [Canvas](https://osysharp.com/reference/ui/canvas/) is drawn on.

It is **not a reaction**. [on change](https://osysharp.com/reference/ui/on-change/) re-runs when a value it read changes; a frame body is driven by *time* and
typically reads nothing that changes on its own, so wiring it as a reaction would either never re-run or spin.

```osy syntax
on frame (double dt) {
  elapsed = elapsed + dt;             // `dt` is SECONDS since the previous frame
  x = x + speed * dt;                 // scale by dt, and the motion is frame-rate independent
}
```

Five things about it are worth knowing before you write one:

- **`dt` is required, and it is seconds.** A body that does not scale by elapsed time runs at whatever speed the
  display happens to be — the defining bug of a hand-rolled loop, and invisible on the machine it was written on.
  You name the parameter; `dt` is the convention, not the contract.
- **It is clamped at 100ms.** Return to a backgrounded tab, or hit a long pause, and the real gap can be seconds; an
  unclamped `dt` would teleport everything through walls in one step. Past 100ms the loop runs *slow* rather than
  *wrong* — so a body cannot measure its own frame rate below 10fps from `dt`. Use `DateTime.UtcNow` if you need the
  true elapsed time.
- **It pauses while the tab is hidden**, and the first frame back is an ordinary one rather than one carrying the
  whole absence.
- **It never re-enters.** The next frame is requested only after the body returns, so a slow body slows the loop
  down instead of queueing copies of itself.
- **A body that throws stops the loop, loudly**, and logs the error with its stack. Sixty identical errors a second
  would bury the first one — the only report anybody could act on. The symptom is "it worked, then froze"; read the
  log.

It is **client-side** by construction — it is driven by the browser's frame clock — so a body that reaches a server
function is refused at compile time. A round trip per frame is never what anyone meant.

## Examples       {#examples}
A create form, symmetric with its edit form — `on mount` seeds the draft; the inputs bind to it; Create commits:

```osy title="org-create" test app=ui-lifecycle
entity Organization { string Name; string Slug; }

[Page("/org/new")]
[Render(CSR)]
component OrgCreatePage() {
  Organization draft;
  on mount { draft = new Organization {}; }

  action Create() { UnitOfWork.Commit(); Navigation.Close("/org/new", true); }

  render {
    Stack(gap: 4) {
      Input(value: draft.Name, placeholder: "Organization name");
      Input(value: draft.Slug, placeholder: "team-slug");
      Button("Create", onPress: Create);
    }
  }
}
```

Both bookends on one page — set a title at open, log the close at teardown:

```osy title="mount-and-unmount" test app=ui-lifecycle
[Page("/report")]
[Render(CSR)]
component Report() {
  string range = "";
  on mount { range = "last-30-days"; }     // prime a filter the moment the page opens
  on unmount { Log.Information("report closed"); } // a parting side-effect, after any child region tears down

  render { Text(range); }
}
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the component these live in, and its other members (fields, `action`, server reads).
- [on change](https://osysharp.com/reference/ui/on-change/) — the reactive sibling: a block that re-runs *whenever* a value it read changes, not just once.
- [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) — the whole execution model: declarations vs `on mount` vs `on change` vs `on unmount`.
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — `new Entity{ … }` and `UnitOfWork.Commit()`, and the two-way `Input(value: entity.Field)` binding.
- [routes and pages](https://osysharp.com/reference/ui/routing/) — binding the page to a route (a create page is just another route).


---

<!-- https://osysharp.com/reference/ui/on-settled/ -->

# on settled — run something once, when a stream finishes

> `on settled(x) { … }` runs once when the stream `x` finishes arriving — whether it completed or failed. It is how you store a streamed answer, and it runs exactly once without you having to guard it.

<!-- id: ui-on-settled · area: ui · stability: preview · html: https://osysharp.com/reference/ui/on-settled/ -->

## Summary        {#summary}
A `stream<T>` arrives in pieces and then stops. `on settled(x) { … }` is the block that runs when it stops — once,
whichever way it ended. That is where you save what arrived.

## Signature      {#signature}
```osy syntax
on settled(<a live var holding a stream>) { … }
```

## Description    {#description}
A component that watches a stream usually has something to do when it finishes: store the answer, mark the
conversation read, move on to the next step. `on settled` is that block.

**It runs exactly once, and you do not write a guard for it.** It is attached to the moment the stream *stops being
open*, which happens once and cannot happen twice. This matters more than it sounds, because the obvious alternative
does not work:

```osy title="✗ on change re-fires on every later render" syntax
on change { if (answer.Done) { Save(chatId, string.Concat(answer)); } }   // ✗ runs again and again
```

`on change` is a reaction — it re-runs whenever a value it read changes. `answer.Done` stays true once the stream is
done, so the block fires again on every later render. In a real app this stored one reply fourteen times.

**It runs when the stream FAILS too**, not only when it completes. A reply that broke off halfway still said what it
said, and the pieces that arrived are still there — so a block that saves the result gets to save the partial. If you
need to tell the two apart, ask the stream:

```osy title="settled runs on failure too — ask which it was" syntax
on settled(answer) {
  if (!answer.Failed) { Save(chatId, string.Concat(answer)); }
}
```

**It may write the component's own state**, which an `on change` block may not. A block that runs once cannot loop,
so there is nothing to protect against — the same reason `on mount` may.

**Name the stream.** A component can watch more than one, so `on settled` always says which: `on settled(answer)`.
Naming something that is not a stream is a compile error, and so is giving one stream two settle hooks — both would
run and the second one's writes would win, so put everything in one block.

**If nobody is watching, it does not run.** The hook belongs to the component, so a reader who navigates away before
the answer finishes never triggers it. Anything that must happen regardless belongs on the server, in the function
that produces the stream.

## Examples       {#examples}

Store a model's reply when it finishes — the whole reason the block exists:

```osy test app=chat-settle
entity Reply { string? Body; }

stream<string> Answer() {
  yield return "the wire ";
  yield return "is live";
}

void Store(string text) {
  new Reply { Body = text };
  UnitOfWork.Commit();
}

component Chat() {
  live var answer = Answer();
  on settled(answer) { Store(string.Concat(answer)); }

  render {
    Stack { Markdown(string.Concat(answer), streaming: !answer.Done); }
  }
}
```

## See also       {#see-also}
- [LlmClient.Stream — a model's answer as it is written](https://osysharp.com/reference/function/llm-stream/) — producing the stream this block waits on
- [on change](https://osysharp.com/reference/ui/on-change/) — for a value that keeps changing, rather than a one-time finish


---

<!-- https://osysharp.com/reference/ui/on-enter/ -->

# onEnter

> `onEnter` runs an action when the Enter key is pressed while an element is focused — the keyboard peer of `onClick`. There is no form to submit: in Osy# a field writes straight into your state (or an entity, as you type), so "pressing Enter" just runs the action that commits, exactly like clicking the button would.

<!-- id: ui-on-enter · area: ui · stability: stable · html: https://osysharp.com/reference/ui/on-enter/ -->

## Summary        {#summary}
`onEnter` binds an **action** to the Enter key, so a keyboard user can submit a field without reaching for the
mouse:

```osy syntax
component SignInCard() {
  string email = "";
  string password = "";
  action SignIn() { Session.SignIn(Login(email, password)); }
  render {
    Stack(gap: 3) {
      Input(value: email,    onEnter: SignIn);
      Input(value: password, type: "password", onEnter: SignIn);
      Pressable(onClick: SignIn) { Text("Sign in"); }
    }
  }
}
```

Enter in either field runs `SignIn`, just as clicking the button does.

That fragment shows only the binding. **A working sign-in also needs** the `Login` function marked `[AuthMethod]`
(so a signed-out visitor may call it at all) and wired into `app.AuthBootstrap` — the compiler enforces that pairing
both ways. [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/) carries the whole thing as one compiled example.

## Signature      {#signature}
```osy syntax
Input(value: email, onEnter: SignIn) — run an action when Enter is pressed in the field
```

## Description    {#description}

### There is no form to post   {#no-form}
In the web platform a `<form>` serializes its fields and POSTs them to a URL, and pressing Enter is what triggers
that POST. **Osy# has no such POST.** A bound `Input` writes its value straight into your component's state — or,
for an entity field, into the row through the page's edit session **as you type** — so by the time you press Enter
there is nothing to submit: the data is already there. "Submitting" is just **running an action** that commits it.

So `onEnter` is not a form-submit affordance dressed up. It is the **keyboard peer of `onClick`**: both run an
action, one on click, one on the Enter key. Reach for it wherever pressing Enter should do the same thing a button
would — a login, a search box, an inline "add row" field.

### What `onEnter` takes — an action, not a submit   {#action}
`onEnter` takes an action, exactly like the other event props (`onClick`/`onInput`/`onChange`/`onBlur`). It can be
a bare action name or one bound with arguments:

```osy title="a bare action, and one bound with arguments" test app=ui-on-enter
[Principal] entity Person { [Required] string Email; }

entity Note {
  [Required] string Title;
  security { allow create, read, update when IsAuthenticated; }
}

[Page("/notes")] [Render(CSR)]
component NoteSearch() {
  string query = "";
  string draft = "";
  var hits = Note.Where(n => n.Title == query).ToList();

  action Search() { /* the query is already in state — this is where you act on it */ }
  action AddItem(string title) { var n = new Note { Title = title }; draft = ""; }

  render {
    Stack(gap: 3) {
      Input(value: query, onEnter: Search);               // a bare action name
      Input(value: draft, onEnter: () => AddItem(draft)); // bound with an argument
      foreach (var h in hits) { Text(h.Title); }
    }
  }
}
```

Enter while the field is empty still fires — the action decides what to do (a login action with a blank password
just fails and leaves you on the page).

### Can `onEnter` go on something other than an `Input`?   {#any-element}
`onEnter` is a general event prop, not an `Input`-only one. Put it on any focusable atom where Enter should act —
an `Input`, a `Pressable`, a search box built from a `Box`. A key that is not Enter does nothing, and an Enter that
is confirming an IME candidate (composing CJK text, say) is ignored, so it never submits the half-typed word.

## See also   {#see-also}
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — how a bound field writes state / an entity as you type (the reason there's nothing to post).
- [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) — `on change` and the once-only lifecycle bodies, for reactions that aren't triggered by a key.


---

<!-- https://osysharp.com/reference/ui/on-escape/ -->

# onEscape

> `onEscape` runs an action when the Escape key is pressed while the element is on screen. Unlike `onEnter` it is not scoped to the focused element — the thing being dismissed is open, not focused — so it listens page-wide, and only the innermost open surface responds to a given keystroke.

<!-- id: ui-on-escape · area: ui · stability: stable · html: https://osysharp.com/reference/ui/on-escape/ -->

## Summary        {#summary}
`onEscape` binds an action to the Escape key, for the universal "get me out of this" gesture:

```osy syntax
component Dropdown() {
  bool open = false;
  action Close() { open = false; }
  render {
    Pressable(onClick: () => open = true) { Text("Options"); }
    if (open) {
      Box(onEscape: Close) {
        Text("…menu…");
      }
    }
  }
}
```

Escape closes the menu whether or not anything inside it has focus.

## Signature      {#signature}
```osy syntax
onEscape: <action>
```

## Description    {#description}

### It is NOT `onEnter`'s mirror image, and that is the whole design   {#vs-onenter}
The two look like a pair and behave differently, because they answer different questions:

| | what it acts on | so it listens |
|---|---|---|
| [onEnter](https://osysharp.com/reference/ui/on-enter/) | the thing you are **on** — confirm this field | on the focused element |
| `onEscape` | the thing that is **open** — dismiss this overlay | page-wide, while mounted |

A popover, a drawer, a lightbox, a dropdown — almost none of them hold focus, because the pointer opened them.
Scoped to the element, `onEscape` would compile, wire up correctly, and then never fire for the single case it exists
to serve. So it listens for as long as the element is on screen, and stops the moment it leaves.

### Nested overlays close one layer per keystroke   {#nesting}
Only the **innermost** live handler runs. A dialog opened over a drawer closes the dialog; pressing Escape again
closes the drawer. Every overlay on the page does not collapse at once, which is what a page-wide listener would do
if each one answered independently.

### It is not blocked by a busy control   {#not-guarded}
Deliberately, `onEscape` is not treated as an activation: it has no in-flight guard and no busy spinner. Dismissing
is local — an overlay you cannot close **because something else on it is still loading** is a worse failure than a
double dismissal, which does nothing anyway.

### An IME keystroke is not yours   {#ime}
Escape also cancels an input-method candidate list. That keystroke belongs to the IME, so it does not run your
action.

## Examples       {#examples}

```osy title="a dismissable drawer" test app=ui-on-escape
component Shell() {
  bool drawer = false;
  action Open()  { drawer = true; }
  action Close() { drawer = false; }
  render {
    Pressable(onClick: Open) { Text("Menu"); }
    if (drawer) {
      Box(onEscape: Close, p: 4) {
        Pressable(onClick: Close) { Text("Close"); }
      }
    }
  }
}
```

Note that the button and the key run **the same action**. That is the pattern to keep: a dismissal reachable only by
pointer is unreachable for anyone not using one, and two separate code paths drift.

## See also       {#see-also}
- [onEnter](https://osysharp.com/reference/ui/on-enter/) — Enter as the keyboard peer of a click, scoped to the focused element
- [keys](https://osysharp.com/reference/ui/keys/) — declaring a key surface, and reading whether a key is held right now
- [component](https://osysharp.com/reference/ui/component/) — state, actions, and the render block these examples are written in


---

<!-- https://osysharp.com/reference/ui/authorize/ -->

# page authorization (policies)

> A `policy` names a reusable authorization predicate over the current user — e.g. `policy Admins => UserRole.Any(r => r.User == user && r.Role == Role.Admin)`. A page marks itself `[Authorize(Admins)]` to require it: an authenticated user who does not satisfy the predicate is refused the page. The reference is compile-checked — `[Authorize(Typo)]` is a build error, not a silent hole.

<!-- id: ui-authorize · area: ui · stability: stable · html: https://osysharp.com/reference/ui/authorize/ -->

## Summary        {#summary}
A **`policy`** names a reusable **authorization predicate** — a condition over the current user (`user`) that says
who is allowed. A page requires a policy with **`[Authorize(Name)]`**; a logged-in user who does not satisfy the
predicate is refused the page (the server returns it to no one who fails the check).

```osy syntax
policy Admins => UserRole.Any(r => r.User == user && r.Role == Role.Admin);

[Page("/admin")]
[Authorize(Admins)]
component AdminPanel() { render { Text("secret"); } }
```

The reference is a **checked symbol, not a string**: `[Authorize(Admins)]` names the declared `policy Admins`, so a
typo (`[Authorize(Admin)]`) is a **compile error**. A policy is declared once and reused across as many pages as need
it.

## Signature      {#signature}
```osy syntax
policy Name => <predicate over `user`>;   // a reusable named authorization predicate
[Authorize(Name)] component Page() { … }  // require it — the principal must satisfy Name
```

The predicate is ordinary Osy#: it reads `user` (the current principal) and queries your data. A role check is the
common shape — `RoleEntity.Any(r => r.User == user && r.Role == Role.X)` — but any boolean predicate over the user
works.

## Description    {#description}
Pages are **protected by default** (see [routes and pages](https://osysharp.com/reference/ui/routing/)): a routed `component` requires an authenticated principal
unless it is `[AllowAnonymous]`. `[Authorize(Name)]` narrows that further — the authenticated principal must also
**satisfy the named policy**. If they don't, the page is refused.

- **`policy Name => <predicate>;`** — declares the predicate once, by name. It reads `user` and may query entities
  (typically a role-grant table). The same policy can gate many pages.
- **`[Authorize(Name)]`** — attaches the policy to a page. The reference is compile-checked against the declared
  policies; an unknown name fails the build.
- **Fail closed** — a policy that cannot be resolved or evaluated refuses the page. If the named policy is missing or
  its predicate errors, the page is refused, never served — authorization never falls open.

Authorization is enforced on the **server**, when the page's definition is requested — so it holds regardless of
what the client does.

## Examples       {#examples}
A role-gated admin page — a reusable `Admins` policy plus a page that requires it:

```osy title="admin-page" test app=ui-authorize
[Principal] entity User { string Email; }
[Role] enum Role { Admin, Viewer }
entity UserRole { [Required] User User; [Required] Role Role; }

policy Admins => UserRole.Any(r => r.User == user && r.Role == Role.Admin);

[Page("/admin")]
[Render(CSR)]
[Authorize(Admins)]
component AdminPanel() {
  render { Text("Admin only"); }
}
```

## See also       {#see-also}
- [routes and pages](https://osysharp.com/reference/ui/routing/) — protected-by-default routing and `[AllowAnonymous]`
- [component](https://osysharp.com/reference/ui/component/) — the page a policy gates
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — a create obeys the entity's write permissions, a related access check


---

<!-- https://osysharp.com/reference/ui/pointer/ -->

# pointer

> `onPointerEnter` / `onPointerLeave` run an action when the pointer enters or leaves an element. They are the half of hover an app can ACT on — a `Hover` variant can only change how something looks, and cannot tell the app anything. Neither carries coordinates, and neither bubbles, which is what makes "am I over this one?" answerable.

<!-- id: ui-pointer · area: ui · stability: stable · html: https://osysharp.com/reference/ui/pointer/ -->

## Summary        {#summary}
`onPointerEnter` and `onPointerLeave` run an action when the pointer enters or leaves an element.

They exist because hover had only ever been a **look**. `inside Card.Hover { Bg = Accent; }` lowers to a `:hover`
rule, so it can restyle an element and cannot tell the app anything — which left *"am I over this drop target?"*
with no spelling at all.

## Signature      {#signature}
```osy syntax
// WHICH element — no coordinates.
Box(onPointerEnter: <action>, onPointerLeave: <action>)

// WHERE on the element — each action takes a `Point`.
Canvas(onPointerDown: <action>, onPointerMove: <action>, onPointerUp: <action>)

class Point { public double X; public double Y; }   // element-relative, in CSS pixels
```

## Description    {#description}
**Neither carries coordinates, and that is the point.** The element already knows the pointer is over it — that is
what a pointer event *is*. Answering "which lane am I over?" with coordinates plus hit-testing is a much larger
surface, and it is the wrong tool for a question the DOM has already answered.

**Neither bubbles.** They lower to `pointerenter`/`pointerleave`, the non-bubbling pair, deliberately: the bubbling
alternative fires on every ancestor of whatever the pointer is really over, so a lane containing a card would report
itself entered when the pointer merely crossed the card — and telling the two apart would need the coordinates back.

**They are not activations.** They never spin a control's busy affordance and never take part in the double-submit
guard. A pointer crossing an element is not a submission, and a drop target that greyed itself out when you hovered
it would be the worse failure.

**Reach for a `Hover` variant when only the LOOK changes.** It is CSS, so it costs no round trip and no re-render,
and it keeps working when the app is busy. Reach for these when the app must *know* — a drop target that has to
record which lane is active, a row that loads a preview, a chart that reports what is under the pointer.

## Where was the pointer? — element-local coordinates   {#coordinates}
`onPointerDown` / `onPointerMove` / `onPointerUp` each hand their action a **`Point`** — where the pointer is, in
**the element's own coordinates**. `(0, 0)` is the element's top-left corner, whatever the page has done around it.

That is the space every consumer actually wants: a canvas has no elements to hit-test, so *"where in THIS canvas"* is
the only question there is, and `Draw.Circle(at.X, at.Y, 4, ink)` needs no conversion. A viewport coordinate would be the
raw browser answer and useless alone — turning it into an element coordinate needs the element's own origin, which
would then have to be kept in sync with every scroll and reflow.

**All three phases, because a gesture has three.** A brush, a zoom or a lasso anchors on `down`, stretches on `move`
and commits on `up`. `onPointerMove` alone cannot express a drag at all: there is no way to learn when it started or
that it ended.

**`onPointerMove` is coalesced to one run per animation frame**, keeping the newest position. A pointer fires at
60-120Hz while a canvas redraws once a frame, so the extra runs are waste — and the alternative default, firing
everything and expecting a `debounce:`, makes the expensive case the one you get by forgetting. `down` and `up` are
discrete and always fire immediately: a coalesced press is a lost click.

> **Reach for enter/leave when the DOM already knows the answer.** *"Am I over this lane?"* needs no coordinates, and
> answering it by hit-testing a `Point` re-derives something the browser has already computed.

> **They fire for touch and pen too**, since they are pointer events rather than mouse events. On a touch screen
> "enter" arrives with the tap, so an interaction that is only reachable by hovering is unreachable there — give it
> a tap or keyboard path as well.

## Right-click and long-press — `onContextMenu`   {#context-menu}

`onContextMenu` runs an action on the **secondary** click — right-click with a mouse, the long-press menu on touch.

```osy syntax
Pressable(onClick: Reveal, onContextMenu: Flag) { Text(face); }
```

**It suppresses the browser's own menu, and that is the point rather than a convenience.** The native menu cannot be
prevented any other way, so an element that drew its own would get both, stacked. Binding the prop *is* the statement
that this element owns its secondary action — bind it only where you mean to replace the browser's.

Like `onClick`, the **innermost** binding wins: a right-click on a card inside a row runs the card's action and not
the row's. Unlike `onClick`, it is **not an activation** — it never spins a control's busy affordance, because
opening a menu is not a submission.

⚑ **There is no `onRightClick`, `onAuxClick`, `onLongPress`, `onMouseDown` or `onDoubleClick`.** One event covers
the gesture on both pointer and touch, and the pointer props above cover everything else — the platform has no
ambient for "which button is down", so `onPointerDown` cannot tell a left press from a right one.

## Examples       {#examples}
```osy title="a drop target that knows which lane the pointer is over" test app=ui-pointer-lanes
[Page("/lanes")]
[Render(CSR)]
[AllowAnonymous]
component Lanes() {
  var over = "none";

  action EnterTodo() { over = "todo"; }
  action EnterDone() { over = "done"; }
  action Clear() { over = "none"; }

  render {
    Stack(gap: 2) {
      Text($"over={over}");
      Box(p: 3, onPointerEnter: EnterTodo, onPointerLeave: Clear) { Text("To do"); }
      Box(p: 3, onPointerEnter: EnterDone, onPointerLeave: Clear) { Text("Done"); }
    }
  }
}
```

```osy title="a canvas that paints where the pointer is" test app=ui-pointer-canvas
[Page("/paint")]
[Render(CSR)]
[AllowAnonymous]
component Paint() {
  var drawing = false;

  action Start(Point at) { drawing = true; }
  action Stroke(Point at) { if (drawing) { Draw.Circle(at.X, at.Y, 4.0, "#3b82f6"); } }
  action Stop(Point at) { drawing = false; }

  render {
    Canvas(w: 400, h: 300, onPointerDown: Start, onPointerMove: Stroke, onPointerUp: Stop);
  }
}
```

## See also       {#see-also}
- [style props](https://osysharp.com/reference/ui/styling/) — `inside X.Hover { … }`, the CSS-only form to prefer when only the look changes
- [[testing-ui#hover]] — `Ui.Hover(control)` moves a real pointer under `osy test --pixels`, which is what actually
  fires `onPointerEnter`/`onPointerLeave` in a test; plain `osy test` locates but cannot move a pointer
- [keys](https://osysharp.com/reference/ui/keys/) — the keyboard counterpart: declaring the keys an element owns, and reading held state
- [drag](https://osysharp.com/reference/ui/drag/) — dragging an element, which is a separate binding and not built on these
- [component](https://osysharp.com/reference/ui/component/) — where an `action` is declared


---

<!-- https://osysharp.com/reference/ui/control-probe/ -->

# probe — what a control says about itself

> A `probe { }` block declares the facts a control publishes about its OWN internal state, so an app's tests can ask for them. It is the one direction across the contract that is not for the app: props go in, events come out, commands go in, and none of them answers "is the document dirty".

<!-- id: ui-control-probe · area: ui · stability: stable · html: https://osysharp.com/reference/ui/control-probe/ -->

## Summary        {#summary}
A control's contract points three ways: **props in, events out, commands in**. All three are for the app — they are
how it drives the control and hears back from it. None of them answers the question a *test* asks: **is the document
dirty right now? how many matches did the search find? what is selected?**

Those facts reach the screen, if at all, as **pixels** — a dot beside a filename, a tinted range inside a
contenteditable, a count the app happened to choose to render. A test could only assert them by reading rendered
text, which measures the control's *styling* rather than its behaviour and breaks the day somebody edits a label. For
a control that paints into a canvas, or one running under a headless DOM that lays nothing out, there is no text to
read at all.

A **`probe { }`** block is the control author's answer: a small, typed surface of facts the control publishes about
itself, asserted with [`Assert.Probe`](#asserting).

**This is how you test a control you did not write.** The alternative is reverse-engineering somebody else's DOM and
coupling every assertion to internals their next release is free to change.

The block is optional, and a control that declares none is unaffected.

## Signature      {#signature}
```osy title="what the control author declares" syntax
control <Name> {
  probe {
    <ScalarType> <fieldName>;
    …
  }
}
```

```osy title="what a test then asserts" syntax
Assert.Probe("<ControlName>", "<fieldName>", <expected>);
```

## Description    {#description}

### Declaring what a test may read — the `probe` block   {#declaring}

```osy title="an editor that publishes what a test needs" test app=ui-control-probe
control MarkdownEditor {
  contractVersion "1.1"
  participation headless
  props { string title; }
  events { dirty(bool isDirty); }
  probe {
    /// Unsaved edits are pending.
    bool dirty;
    /// How many find-hits the document currently holds.
    int matches;
    /// What is highlighted right now, or nothing when the selection is empty.
    string? selection;
  }
}
```

A field's type is a **comparable scalar** — `bool`, `int`, `long`, `double`, `decimal`, `string`, and their nullable
forms. An assertion compares one value against one expected value, so a shape or an array has nothing it could be
compared to; a control that wants to publish a structure publishes the parts of it a test can name, one field each.

**A default is refused.** A prop has a resting value because the app may decline to set one. A probe field is what is
true at the instant it is read, so a default would be a value the control reports *while stating nothing* — and every
assertion against it would pass, including against a shim that never answers at all.

### A probe is not an event    {#not-an-event}

`dirty` above is both an event and a probe field, and that is not a duplication.

An **event** is *"this just changed"*. A **probe** is *"this is true now"*. A test that had to listen for a
notification in order to learn a resting fact could not ask the question at all until the control happened to change
its mind — so a page that opens with unsaved work, or a search already showing results, would be unassertable.

Publish as an event what the app must react to; publish as a probe what somebody may need to ask.

### The shim must implement it    {#implementing}

The block generates a `probe()` method on what `mount` RETURNS, typed from the declaration:

```ts
export interface MarkdownEditorHandle extends ControlHandle<MarkdownEditorProps> {
  probe(): {
    dirty: boolean;
    matches: number;
    selection?: string | null;
  };
}
```

```ts syntax
export function mount(el, props, host) {
  return {
    update(next) { … },
    destroy() { … },
    probe() {
      return {
        dirty,
        matches: findState.hits.length,
        selection: selected.length > 0 ? selected : null,
      };
    },
  };
}
```

Three properties are part of the contract — and a fourth for a control that finishes starting **after** `mount`
returns: hand back `ready`, a promise that rejects when the start failed, so the probe reports *never started* rather
than a value from a control that is not running (see [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/)).


- **It is a METHOD, never a property.** A probe is what is true at the instant it is read. A property would be read
  once at mount and then handed out as a snapshot that stopped being true minutes ago.
- **It must not change anything.** Read from live state; do not dispatch, do not move a selection, do not save. A
  probe that perturbed what it was measuring would prove nothing about it.
- **It must be cheap.** It is called on demand, once per assertion.

**The platform checks it when it takes the handle** — at the mount that produced it, exactly as it checks `update`,
`destroy` and the [command bag](#see-also). A control that declares a `probe { }` block and returns no `probe()`
fails there, naming the fields it published. The alternative is that the shortfall is found by whoever first asks,
which is a test, whose refusal then reads as *"this control reports nothing"* — a sentence about the control's
behaviour, for a defect in its handle.

### Asserting on a probe in a UI test   {#asserting}

```osy title="asserting a control's own state" syntax
Ui.Visit("/doc");
Ui.Fill("Body", "some new text");

Assert.Probe("MarkdownEditor", "dirty", true);
Assert.Probe("MarkdownEditor", "matches", 3);
Assert.Probe("MarkdownEditor", "selection", null);
```

Three operands — the control, the field, the expected value — the same shape as
`Assert.Cell(row, column, expected)`.

The control is named by its **declared name**, which is what the app wrote at the call site. A mount element carries
no accessible name of its own, so when a page renders the same control twice they are told apart by
[`within:`](#two-instances).

`null` means *the control reports nothing for this field*, which is a different statement from the empty string: a
caret sitting in a document selects nothing at all, and an assertion for `""` must not pass on it.

Each way it can fail says something different, because each has a different fix:

| What is wrong | What you are told |
|---|---|
| No such control is mounted | Which controls *are* mounted here |
| The control implements no `probe()` | That its shim owes one — not that the field is misspelled |
| It does not report that field | The fields it does report |
| It reports a different value | What it actually said |

### Two instances of one control    {#two-instances}

A page may legitimately render the same control twice. `within:` narrows by which container each one mounted into —
the same argument every other UI assertion takes:

```osy syntax
Assert.Probe("MarkdownEditor", "dirty", true,  within: "Draft");
Assert.Probe("MarkdownEditor", "dirty", false, within: "Published");
```

Without a scope, two mounted instances are an **ambiguity and are refused** rather than resolved by picking the
first — which would answer a question nobody asked, and differently depending on render order.

### Which facts belong in a probe?   {#what-to-publish}

Publish what a test needs and a screen does not carry. A probe field is part of the contract like everything else in
the block, so it can be relied on and versioned — and it is equally a decision to leave something out.

Prefer a **prop** for something the app sets, an **event** for something it must react to, a **command** for
something it asks for, and a **probe field** for something it (or its tests) may need to *know*.

Do not mirror the props back: the app already has those values, and reporting them says nothing about whether the
control did anything with them. `dirty` is worth publishing precisely because nothing outside the control knows it.

## Examples       {#examples}

A chart that reports what it actually drew — the classic case, because a canvas has no DOM to read:

```osy title="a control whose output is pixels" test app=ui-control-probe-chart
control LineChart {
  contractVersion "1.1"
  participation opaque
  props { string title; }
  probe {
    /// How many series were plotted after filtering.
    int seriesDrawn;
    /// The point the pointer is currently over, or nothing.
    string? hoveredPoint;
    /// The chart finished its entry animation and is at rest.
    bool settled;
  }
}
```

Nothing here is visible to a DOM query: the chart is one `<canvas>`. Without a probe, the only assertable fact about
it is that the element exists — which is true of a chart that drew nothing.

## See also       {#see-also}
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the `control` block these are declared in
- [commands — the verbs a control accepts](https://osysharp.com/reference/ui/control-commands/) — the verbs a control accepts, the mirror of its events
- [Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/) — the UI test surface `Assert.Probe` belongs to


---

<!-- https://osysharp.com/reference/ui/read-in-a-body/ -->

# reading data inside an action

> A data read written inside an `action`, `method` or lifecycle hook runs at that point in the body: the platform fetches it from the server and the body continues with the rows. It is not a page-load value and has no `var` / `live var` decision to make, because a body runs once — when the user triggers it.

<!-- id: ui-read-in-a-body · area: ui · stability: stable · html: https://osysharp.com/reference/ui/read-in-a-body/ -->

## Summary        {#summary}

Write the read where you need the answer.

```osy syntax
action Refresh() {
  var rows = Order.Where(o => o.Code == code);
  found = rows.Count();
}
```

The read happens on the server, when the click happens. The body waits for it and carries on — the same way it waits
for a server function it calls.

## Signature      {#signature}

```osy syntax
action <Name>(…) {
  var <rows>  = <Entity>.Where(…).OrderBy(…);          // the rows
  var <one>   = <Entity>.SingleOrDefault(…);           // one row, or null
  var <n>     = <Entity>.Count(…);                     // how many
  var <any>   = <Entity>.Any(…);                       // whether any
  var <total> = <Entity>.Sum(x => x.<Column>);         // a computed value
}
```

The same is true in a `method`, an `on mount`, an `on change` and every other imperative member. A **render**
expression is a different question — see [component](https://osysharp.com/reference/ui/component/).

## Description    {#description}

### Why this is not the same as a page-load query   {#vs-declaration}

A component member is a **binding**: it is re-evaluated as the page renders, so declaring one makes you choose what
should happen when its inputs change — `var` fetches once and can go stale, `live var` re-reads and pays a round trip
each time. That choice is real, and it is why a read cannot simply be hoisted for you.

**A body is not a binding.** It runs once, at the moment the user triggers it. There is no "when should this happen
again?", nothing can go stale, and there is no decision for anyone to take. So the read simply runs, there, then.

That distinction is the whole feature, and it decides where to put a read:

| you want | write it |
|---|---|
| something the page SHOWS | a member — `var` for a snapshot, `live var` to follow changes |
| something an action NEEDS in order to act | the read, in the action |

Putting an action's read on a member is not merely more code. The member is fetched **with the page** — before the
user did the thing that made them want the answer — so the action would act on a picture from earlier.

### What it costs   {#cost}

A round trip, at that point in the body. That is what you asked for by writing it there, and an action is already a
multi-request thing. Two reads in a body are two round trips, in order; if you need them together, ask once.

### Only VALUES cross to the read   {#values-cross}

The read runs on the server. Values you hold travel to it — a local, a component member, a route parameter, the id of
a row you are showing:

```osy title="✓ only values cross — send the id" syntax
action Look(Customer c) {
  var theirs = Order.Where(o => o.Customer.Id == c.Id);   // ✓ a Guid crosses
}
```

A whole **row** does not travel. Comparing against one is refused, and the compiler names the row and shows you the
id form:

```osy title="✗ a whole row cannot cross to the server" syntax
action Look(Customer c) {
  var theirs = Order.Where(o => o.Customer == c);         // ✗ refused: `c` is a row
}
```

This is the same rule everywhere in the language, said once: a row on the client is its identity, and its fields live
in the store. Nothing about it is special to a body.

### What comes back   {#result}

Rows come back as rows: read their fields, loop them, filter them further in memory.

```osy title="what comes back is rows — filter further in memory" syntax
action Look() {
  var rows = Order.Where(o => o.Total > 100);
  var big  = rows.Where(o => o.Priority);     // in memory, no second round trip
  foreach (var o in rows) { Log.Information("{Code}", o.Code); }
}
```

They also land in the page's own store, so a row you then edit is the row the page is already showing — not a
detached copy of it.

### Security is not a question here   {#security}

The read goes out under your own principal and comes back through the same secured read every other query uses. A
body cannot ask for more than the page could, and there is nothing to check or arrange: see [security { }](https://osysharp.com/reference/security/entity-security/).

## Examples       {#examples}

Look something up on a click and show the answer:

```osy title="read on the click" test app=ui-read-in-a-body
entity Note {
  [Required, MaxLength(80)] string Title;
  int Rank;
  security { allow read when IsAnonymous || IsAuthenticated; }
}

[Page("/look")]
[Render(CSR)]
[AllowAnonymous]
component LookPage() {
  int total = -1;

  action Look() {
    var rows = Note.Where(n => n.Rank > 1);
    total = rows.Count();
  }

  render {
    Stack(gap: 2) {
      Text("total:" + total);
      Button("Look", onPress: Look);
    }
  }
}
```

Key the read off something the page holds — the value is read at the click, not at page load:

```osy title="keyed off a component member" test app=ui-read-in-a-body
[Page("/search")]
[Render(CSR)]
[AllowAnonymous]
component SearchPage() {
  string wanted = "";
  string found = "-";

  action Search() {
    var one = Note.SingleOrDefault(n => n.Title == wanted);
    found = one == null ? "none" : one.Title;
  }

  render {
    Stack(gap: 2) {
      Input(value: wanted, placeholder: "Title");
      Text("found:" + found);
      Button("Search", onPress: Search);
    }
  }
}
```

Ask a question rather than fetching rows to count them:

```osy title="Count, Any and Sum" test app=ui-read-in-a-body
[Page("/tally")]
[Render(CSR)]
[AllowAnonymous]
component TallyPage() {
  int total = 0;
  string state = "-";

  action Tally() {
    total = Note.Sum(n => n.Rank);
    state = Note.Any(n => n.Rank > 2) ? "has-high" : "all-low";
  }

  render {
    Stack(gap: 2) {
      Text("total:" + total);
      Text("state:" + state);
      Button("Tally", onPress: Tally);
    }
  }
}
```

## See also       {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — declaring a component, and its `var` / `live var` members: the read a page SHOWS, and the
  choice that comes with it
- [Session.CurrentUser](https://osysharp.com/reference/ui/current-user/) — the one read that is fetched with the page, because it has no inputs that can change
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — writing in an action, and when the write is committed
- [security { }](https://osysharp.com/reference/security/entity-security/) — who may read what, declared once on the entity


---

<!-- https://osysharp.com/reference/ui/routing/ -->

# routes and pages

> How a component becomes a page: it declares a route with `[Page("/catalog/{slug}")]`, and navigating to a matching path serves it. Covers route templates and param capture, server- vs client-rendering, layouts and retained pages, in-app navigation, and the secure-by-default rule that a routed page requires sign-in.

<!-- id: ui-routing · area: ui · stability: stable · html: https://osysharp.com/reference/ui/routing/ -->

## Summary        {#summary}
A component becomes a **page** when it declares a route: `[Page("/catalog/{slug}")]`. When a browser navigates to a
matching path, the platform serves that page, binding the captured route segments to the component's props. Your app
ships no JavaScript of its own — you write components, and the platform runs them.

## Signature      {#signature}
```osy syntax
[Page("/catalog/{slug}")]        // the route template; {slug} captures a segment
component Catalog(slug) { … }    // …and binds to the same-named prop

[Route("/catalog/{slug}")]       // `[Route]` is the same attribute under a second name
component Catalog(slug) { … }
```

`[Page]` and `[Route]` mean exactly the same thing. Both spellings are accepted everywhere a routed component is
declared, and neither is preferred — pick one and stay with it in a codebase.

## Description    {#description}

### Putting a parameter in a URL — `{name}` segments   {#route-templates}
The route is the string in `[Page(...)]`. Segments are matched literally except `{name}` segments, which capture one
path segment and bind to the same-named component prop.

| Template | Path | Result |
|---|---|---|
| `/login` | `/login` | `Login`, no params |
| `/` | `/` | the root page |
| `/catalog/{slug}` | `/catalog/shoes` | `Catalog`, `slug = "shoes"` |
| `/o/{order}/line/{line}` | `/o/A1/line/7` | `order = "A1"`, `line = "7"` |
| `/plants/{id}` | `/plants/3f2a…` | `Edit`, `Guid id` — see below |

**A captured segment is converted to the PROP'S DECLARED TYPE.** The examples above all bind `string` because that
is what those pages declare, but the type is yours to choose — `Guid`, `int`, `long`, `decimal`, `bool` and
`DateTime` all bind, and a value that will not convert is a 404 rather than a page that throws:

```osy syntax
[Page("/plants/{id}")]
component EditPlant(Guid id) { … }        // `id` arrives as a Guid — no parsing, no Guid.Parse
```

⚑ Worth stating because the absence read as an answer: every example on this page binds a string, `line = "7"`
included, and a generated app duly took `string id` and called `Guid.Parse` on it by hand rather than "risk
relying on automatic type conversion from the route" (measured, eval run 263). The conversion is not a risk; it is
the contract.

Matching rules:
- Segment counts must be equal — `/catalog/{slug}` does **not** match `/catalog` or `/catalog/a/b`.
- Static segments match case-insensitively; captured values are URL-unescaped (`red%20shoes` → `red shoes`).
- **The most specific template wins**: a fully static template beats one with params, so `/catalog/new`
  resolves to the literal `NewItem` page, never to `/catalog/{slug}` with `slug = "new"`.
- A trailing slash is ignored (`/notes` and `/notes/` resolve alike).
- No path matches → the browser gets a **404**.

Only routed **components** are candidates — a component without a `[Page]` is never a destination.

### Render mode — SSR vs CSR {#render-mode}
A page declares how it is delivered with `[Render(...)]`. The mode decides whether the first response already contains
the page's content or the browser renders it after loading:

- **`[Render(CSR)]`** (client-side rendering) — the first response is empty of page content; the browser then renders
  the page. Simplest, but the first visible content waits on the browser to render.
- **`[Render(SSR)]`** (server-side rendering) — the page's content is in the first response, so it is visible
  immediately. The page then **hydrates** in the browser: it becomes interactive (state, events, and `live var`
  updates) without re-fetching or re-drawing what was already shown.

Server-side and client-side rendering produce the **same page** from the same definition — a prop, an interpolated
value, or an `if` condition evaluates identically either way, and an `Input` bound to state shows its current value in
the initial content. SSR is purely an optimization for first paint; choosing it never changes what the page does, only
how quickly its content appears.

**Server rendering is for public pages.** Only a page marked `[AllowAnonymous]` is pre-rendered. A hard browser
navigation carries no session, so the server has no identity to render a private page under — and pre-rendering it as
"nobody" would put a protected page's structure into content anyone can request. A page that requires sign-in is
therefore delivered client-side and renders once the browser has a session. It still works exactly the same; it just
isn't in the first response.

**Data on a server-rendered page.** A public page has its server-read data fetched and rendered on the server too, so a
list of records is already in the first response — ideal for pages that must load fast or be indexed.

**Chrome comes with it.** A server-rendered page inside a `[Layout]` arrives with its layout already painted around
it, so the shell is on screen before any JavaScript runs — see [layouts](#layouts) below.

If a page uses something server rendering doesn't support yet, that page falls back to client-side rendering
automatically — it still loads and works, it just isn't pre-rendered. You never get a broken page for choosing
`[Render(SSR)]`.

### Sharing a sidebar or header across pages — `[Layout]`   {#layouts}
Most apps wrap their pages in shared chrome: a sidebar, a header, a tab bar. Rebuilding that on every navigation is
both slow and visibly wrong — a sidebar shouldn't blink, and its scroll position and open sections shouldn't reset.

A **layout** is a component that wraps child pages. It marks itself with `[Layout]` and renders exactly one `Outlet;`
— the place its child page appears. A page opts in by naming the layout it renders inside:

```osy title="a layout, and the pages that render inside it" test app=ui-routing
[Composable] component Sidebar() { render { Text("nav"); } }

[Layout]
component AppShell() {
  render {
    Sidebar();
    Outlet;          // the matched page renders here
  }
}

[Page("/users")] [Layout(AppShell)] [Render(CSR)] component UsersPage() { render { Text("Users"); } }
[Page("/teams")] [Layout(AppShell)] [Render(CSR)] component TeamsPage() { render { Text("Teams"); } }

[Page("/login")] [AllowAnonymous] [Render(CSR)] component LoginPage() { render { Text("Sign in"); } }   // no layout — renders bare
```

Navigating from `/users` to `/teams` **keeps `AppShell` mounted**. Only the outlet's child is replaced, so the shell's
chrome, its state, its queries, and its scroll position all survive. Navigating to `/login` — which names no layout —
tears the shell down; coming back rebuilds it.

`[Layout(AppShell)]` is a **checked identifier**, not a string: naming something that isn't a layout is a compile
error, exactly like naming an undeclared policy in `[Authorize(...)]`. More mistakes are caught at compile time
rather than becoming a blank screen:

- an `Outlet;` in a component that isn't a `[Layout]`;
- a `[Layout]` that renders **no** outlet (its child would have nowhere to go) or **more than one**;
- a component using **itself** as its layout;
- a **public page inside a protected layout**. An anonymous visitor loads a page's chrome as well as its body, so an
  `[AllowAnonymous]` page can only render inside a layout that is itself `[AllowAnonymous]` (or `[Composable]`).
  Otherwise the page could never paint for the visitor it was made public for.

### Does the layout render on the server too?   {#layout-ssr}
When a `[Render(SSR)]` page declares a layout, the server renders the **layout around it** — the shell and the page
arrive together, in one response. The browser paints the whole thing before the app's JavaScript has loaded, and when
the client takes over it **adopts** what was painted rather than rebuilding it. Nothing flashes and nothing is drawn
twice. This is the reason a layout is compile-checked so heavily: the page the server paints and the page the client
builds have to be the same page.

### Keeping more than one page alive    {#retain}
By default an outlet holds **one page at a time**: navigating away disposes the page you left. That's what a shop, a
marketing site, or any ordinary web app wants, and it costs nothing.

Some apps want the opposite. An admin console where you keep several records open; a mobile shell with a back-stack; a
wizard whose steps remember what you typed. For those, tell the outlet to **retain** the routes you visit:

```osy syntax
Outlet;                  // one page at a time — disposed on navigate (the default)
Outlet(retain: true);    // every visited route stays alive, hidden, exactly one visible
Outlet(retain: 8);       // …up to 8; beyond that the least-recently-used one is dropped
```

A retained page is **mounted, just not visible**. Its state, its scroll position, and any **unsaved edits** survive —
so returning to it is instant and nothing you typed is lost. Its live queries go quiet while it's hidden and catch up
in a single read when you come back, so keeping pages around doesn't multiply your data traffic.

**The platform has no opinion about how you present this.** There is no tab component, no tab bar, no back-stack
widget. Retention is the mechanism; you decide whether it looks like tabs, a stack, a wizard, or nothing at all, and
you build that chrome from ordinary components in your layout. [Navigation](https://osysharp.com/reference/ui/navigation/) is what you read to build it.

Two rules the platform does enforce, because they protect the user's work:

- **A page with unsaved edits is never dropped to satisfy a `retain: N` cap.** The cap is exceeded instead. Losing
  someone's half-finished form to reclaim memory is never the right trade.
- **Closing a page with unsaved edits requires an explicit confirmation from your app.** The platform will refuse the
  close and tell you the page is dirty; showing the dialog (and what it says) is yours.

### Three rules about layouts   {#layout-notes}
- **A layout is not a route.** It has no `[Page]` of its own and never appears as a destination; it exists only to
  wrap pages.
- **A layout wrapping public pages must itself be public.** Page structure is fetched under the same secure-by-default
  rule as everything else, so a layout around `[AllowAnonymous]` pages needs `[AllowAnonymous]` too. A layout around
  protected pages simply stays protected.
- **Layouts don't nest yet.** A `[Layout]` that declares its own `[Layout(...)]` is a compile error rather than
  silently rendering without its parent.

### Why an in-app link does not reload the browser   {#navigation}
Once an app is running, moving between its pages **does not reload the browser**. A link to one of the app's own
routes swaps that page's content in place, leaving the rest of the app — and everything it has already loaded —
alive.

The address bar still holds a **real URL**, never a `#` fragment. So every page in your app is:

- **bookmarkable and shareable** — pasting the URL into a fresh tab opens that exact page;
- **navigable with Back and Forward**, which move between pages rather than out of the app;
- **refreshable** — reloading a deep URL re-serves that page, not the home page.

Route params keep working exactly as they do on a first load: `/catalog/{slug}` navigated to as `/catalog/shoes`
binds `shoes` to the page's `slug` parameter.

To navigate from code — and to read which routes are open, which is active, and which hold unsaved edits — use
[Navigation](https://osysharp.com/reference/ui/navigation/).

### What still performs a full browser navigation    {#hard-navigation}
Only in-app links are taken over. Everything a user expects the browser to handle, the browser still handles:

- a link to a path that **isn't one of your routes** (a file, an API path, a 404);
- an **external** link, or one marked `rel="external"`;
- a link with a **`target`** (e.g. opening in a new tab) or a **`download`**;
- a **modified click** — ⌘/Ctrl-click, Shift-click, Alt-click, or a middle-click.

That last one matters: "open in a new tab" keeps working on every link in your app.

### Signing in mid-navigation    {#navigation-auth}
Navigating to a page that requires authentication while signed out sends the visitor to your login page, carrying a
return address so they land back where they were headed. This is a convenience, not the security boundary — the
server independently refuses to serve a protected page's structure or data to a caller who isn't allowed it, so a
page can never leak by a client-side check being skipped.

### Auth model — secure by default {#auth}
**A routed component requires an authenticated principal BY DEFAULT.** A page opts out with `[AllowAnonymous]` — the
explicit, compiler-visible declaration that it is public. This is the platform's "safe by default" posture applied to
routing: the secure configuration is the zero-config one, and a page can never leak by a forgotten attribute (the
failure mode of the inverse "`[Authorize]` opts in" model).

- `component Home()` — protected: requires an authenticated principal.
- `[AllowAnonymous] component Home()` — public: any caller, including anonymous, may view it.
- `[Authorize(Managers)] component Home()` — protected *and* the principal must satisfy the named `policy Managers`
  (a compile-checked reference, not a string; see [page authorization (policies)](https://osysharp.com/reference/ui/authorize/)).

The **real authorization teeth are at the data boundary**, not at page delivery. A hard browser navigation carries no
credential (the platform authenticates with a Bearer token, not a cookie), so — exactly as with any single-page app —
the server cannot tell a logged-in user from an anonymous one at page-load. So delivery of the page *shell* is public
(a bootstrapper contains no app data), and the `[AllowAnonymous]`-or-not fact rides along only so the browser can
redirect an unauthenticated visitor to the login route before it tries to fetch a page it can't have. The server then
**independently refuses** to serve a protected page's structure or its data to a caller who isn't allowed it — so a
malicious client can load a public shell but can never obtain a protected page's tree or rows.

The login flow itself is `[AllowAnonymous]`, so it loads for a signed-out visitor. The user submits credentials, the
app's own auth surface issues them a session, and every subsequent page and data request is made under it.

## See also {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the `[Page]` / `[Authorize]` / `[Render]` attributes on the component.
- [Navigation](https://osysharp.com/reference/ui/navigation/) — reading the open routes and navigating from code.
- [[ui-component#render-tree]] — the render tree the client walks once the component is mounted.
- <span class="planned" title="this page is planned and not written yet">ui-query-member</span> — how a mounted page's server-read members read data.


---

<!-- https://osysharp.com/reference/ui/skeleton/ -->

# skeleton

> A second render tree that stands in for a component while its first query has not yet arrived. It is written with the same grammar as `render`, it may not read the data it stands in for, and it gives way the moment the first result lands — never again on a refetch.

<!-- id: ui-skeleton · area: ui · stability: stable · html: https://osysharp.com/reference/ui/skeleton/ -->

## Summary        {#summary}
A component that reads data has a moment before that data arrives. `skeleton { }` says what to draw in that moment:
the **shape** the content will take, at the size it will take, so the page is composed before it is filled.

It is an ordinary render tree — same grammar, same controls, same style props — held on the component beside
`render { }`. The runtime shows it while a query of that component has **never settled**, and swaps it for the real
tree the moment the first result lands.

Without one, a component whose read is in flight renders as **nothing**, and the page assembles itself in front of the
user as each piece arrives. That is not merely unpolished: content that appears late pushes what is already on screen,
so a person reading — or aiming at a button — has it move under them.

## Signature      {#signature}
```osy syntax
component Name(<params>) {
  live var <data> = …;          // the read the skeleton stands in for

  render   { … }                // the real tree
  skeleton { … }                // the stand-in — same grammar, no data reads
}
```

At most one `skeleton` block per component, and it is optional everywhere.

## Description    {#description}

### It fills a CHILD's window, not the page's   {#where-it-shows}
A routed page **awaits its own data before the first paint**, so by the time the page exists its queries have already
answered — a page-level skeleton would have nothing to cover. The window this block fills belongs to a **composed
child**: a child's read is deliberately not awaited, so the child paints and then fills. That is the ordinary case for
a kit control or any component you drop into a page.

So put `skeleton` on the component that **does the reading**, not on the page that contains it. A component with no
queries never shows a skeleton, because there is nothing to wait for.

One consequence worth knowing before you hit it: because a skeleton belongs to a composed child, that child needs
[[ui-composable|`[Composable]`]] when the page around it is public. Its own read stays gated either way — the
attribute is about being allowed to *render inside* an anonymous page, not about what it may read.

### It shows on the FIRST read, and never again   {#first-read-only}
The swap is driven by whether a query has ever **settled** — not by whether one is currently loading. Those differ
exactly once, and it matters:

| | |
|---|---|
| First read, nothing on screen yet | the skeleton shows |
| A refetch, with the previous rows still on screen | the skeleton does **not** show |

Replacing rows the user is already reading with grey placeholders is a regression, not a loading state. For the
*in-flight* feedback that a refetch or an action deserves, see [Pending](https://osysharp.com/reference/ui/pending/); for a read that came back **refused or
broken**, see [A failing query](https://osysharp.com/reference/ui/query-failure/) — that region reports the failure in place, and a skeleton would sit there forever
pretending it was still coming.

### A skeleton may not read the data it stands in for   {#no-data-reads}
Reading a `live var` from inside `skeleton { }` is a **compile error naming the member**. This is not a style rule —
it is the block's defining condition. A skeleton renders precisely *because* that read has not arrived, so anything it
reads off it is empty by construction, and the placeholder would silently render nothing.

**Params and plain `var` state are allowed, deliberately.** Both are present the moment the component mounts, and a
skeleton that knows how many rows to draw is a better skeleton than one guessing three.

The check covers the two ways data reaches a render tree: element **arguments** and `foreach` **sources**. A data read
buried in an `if` condition is not caught — it renders as a false branch rather than as anything harmful.

### Draw the shape, at the real size   {#geometry}
The bar to aim for is that **nothing moves when the data arrives**: the skeleton occupies the same box the loaded
content will. A placeholder that is the wrong height is worse than none, because it promises a layout and then breaks
it — the reflow it causes is the exact problem a skeleton exists to prevent.

In practice that means fixing the dimensions rather than letting a placeholder collapse: give each stand-in row the
height its real row will have, and the container the gap it will have.

A skeleton is **static by default**. If you want the shimmer, it is an ordinary [`animation`](https://osysharp.com/reference/ui/animation/) applied
with a style prop — the platform ships no privileged pulse, because the timing is a house-style decision.

### It survives an export   {#round-trip}
`skeleton { }` persists into the application model as a second render tree and is regenerated by the decompiler, so a
component exported and recompiled keeps it. Worth stating only because it did not always: the block was persisted and
read back by nothing for its whole first life, which is invisible in the output — the component still renders, just
with nothing on screen while its first read is in flight, which is the entire point of the block.

## Examples       {#examples}

### A child that holds its shape while its rows load   {#example-child}
```osy title="the ordinary case — the skeleton lives on the component that reads" test app=ui-skeleton
entity Order { [Required] string Reference; decimal Total; }

[Composable]                     // it is dropped into a public page; its own read stays gated
component RecentOrders() {
  live var orders = Order.OrderByDescending(o => o.Total).ToList();

  render {
    Stack(gap: 2) {
      foreach (var o in orders) { Text(o.Reference); }
    }
  }
  skeleton {
    // The same frame, with the row's real height — so nothing reflows when the rows arrive.
    Stack(gap: 2) {
      Box(h: "14px", w: "240px");
      Box(h: "14px", w: "240px");
      Box(h: "14px", w: "240px");
    }
  }
}

[Page("/orders")]
[Render(CSR)]
[AllowAnonymous]
component OrdersPage() {
  render { RecentOrders(); }
}
```

### Sizing the stand-in from a param   {#example-param}
A param is present at mount, so the caller can tell the skeleton how much to draw:

```osy syntax
component RecentOrders(int rows = 3) {
  live var orders = Order.OrderByDescending(o => o.Total).Take(rows).ToList();

  render   { foreach (var o in orders) { Text(o.Reference); } }
  skeleton { foreach (var i in placeholders) { Box(h: "14px", w: "240px"); } }   // `rows` and plain state are fine
}
```

### What the compiler refuses   {#example-refused}
```osy syntax
component RecentOrders() {
  live var orders = Order.ToList();

  render   { foreach (var o in orders) { Text(o.Reference); } }
  skeleton {
    foreach (var o in orders) { Box(h: "14px"); }   // ERROR: a `skeleton` block cannot read 'orders' —
  }                                                 // it renders precisely while that data is still loading
}
```

## See also       {#see-also}
- [Pending](https://osysharp.com/reference/ui/pending/) — the in-flight feedback for actions and refetches, which is the *other* waiting state.
- [A failing query](https://osysharp.com/reference/ui/query-failure/) — when the read comes back refused or broken, that region says so in place.
- [animation — looping motion with no destination state](https://osysharp.com/reference/ui/animation/) — `animation` blocks, if you want the stand-in to shimmer.
- [[Composable] — presentational components in public pages](https://osysharp.com/reference/ui/composable/) — why a composed child needs the attribute to render inside a public page.
- [component](https://osysharp.com/reference/ui/component/) — components, `render`, and the members a skeleton may read.


---

<!-- https://osysharp.com/reference/ui/sound/ -->

# sound

> Drop `.mp3`, `.wav`, `.ogg` or `.m4a` files into `model/sounds/` and play them with `Sound.Play(Sounds.Flap)`. The name is a compile-checked member of your app's `Sounds` vocabulary, so a typo is an error rather than silence. `Sound.Loop` starts background audio and `Sound.Stop` ends it.

<!-- id: ui-sound · area: ui · stability: preview · html: https://osysharp.com/reference/ui/sound/ -->

## Summary        {#summary}
A sound is a **file in your app**, not data. Put a `.wav` in `model/sounds/` and it becomes part of your app's
vocabulary:

```text
model/
  sounds/
    blip.wav
    game_over.mp3
    valley.ogg
```

`blip.wav` is now playable as `Sound.Play(Sounds.Blip)`. There is nothing to register, nothing to import, and no
path to spell.

Sounds are the **fourth kind of asset** an app ships, and they divide by what the file *is*:

| you have | put it in | reach it with |
|---|---|---|
| a single-colour glyph that should follow your text | `model/icons/` | `Icon(Icons.Search)` |
| a vector illustration, logo or background | `model/art/` | `Svg(Art.Hexgrid)` |
| a bitmap — a wall texture, a sprite sheet, a photograph | `model/textures/` | `Draw.Image(wall, …)` |
| **audio** — an effect, a jingle, a music loop | `model/sounds/` | `Sound.Play(Sounds.Blip)` |

## Signature      {#signature}
```osy syntax
Sound.Play(Sounds.Blip)                    // one shot; overlaps with itself and with anything else
Sound.Play(Sounds.Blip, volume: 0.6)       // volume is 0..1; absent means full

Sound.Loop(Sounds.Valley)                  // start looping — background music, an engine hum, rain
Sound.Loop(Sounds.Valley, volume: 0.2)
Sound.Stop(Sounds.Valley)                  // stop that loop
Sound.StopAll()                            // stop every loop
```

Every verb returns nothing. Playback is fire-and-forget from your app's point of view.

## Description    {#description}
The verbs run on the **client**, because that is where the speakers are. You can call them from an ordinary action,
from an `on change` block, or from `on frame` in a game — there is no server round trip and nothing to await.

### The name is a value, not a string   {#names}
`Sounds` is a real type, synthesized from the files you ship. So a sound goes wherever a value goes — into a local,
across a component boundary as a parameter, through a `switch`:

```osy title="a component that takes the sound to make" test app=arcade
[Composable]
component Blipper(Sounds note, string label) {
  action Poke() { Sound.Play(note, volume: 0.5); }
  render { Pressable(label, onClick: Poke); }
}

[Page("/blip")] [AllowAnonymous]
component BlipPage() {
  render {
    Stack(gap: 2) {
      Blipper(note: Sounds.Blip, label: "blip");
      Blipper(note: Sounds.Blip, label: "again, quietly");
    }
  }
}
```

That is why the argument is `Sounds.Blip` and never a bare `blip` or a `"blip.wav"`. Both of those are refused, each
with the rewrite that fixes it:

```text
Sound.Play(blip)        →  `blip` is a declared sound, but a sound is read through its own
                           vocabulary — write `Sounds.Blip`.

Sound.Play("blip.wav")  →  `Sound.Play` takes a declared sound, not a file name — write
                           `Sounds.Blip`. (The app's sounds are compiled in and content-addressed,
                           so there is no path to spell.)
```

### Nothing plays until the visitor has interacted   {#autoplay}
Every browser refuses to start audio until the person has clicked, tapped or typed something on the page, and it
refuses **silently** — no error, no warning, just nothing. This is not something you work around; it is a rule about
consent that every site is subject to.

The platform handles the mechanics: the first gesture the page sees unlocks audio, and everything after that plays
normally. What you have to handle is the **design** consequence — a sound that is meant to play the instant the page
opens will not. Put the first sound behind something the visitor does.

```osy syntax
// ✗ nothing will be heard: the page has had no interaction yet
component TitleScreen() {
  on mount { Sound.Loop(Sounds.Valley); }
}

// ✓ the music starts when they start
component TitleScreen() {
  action Begin() { Sound.Loop(Sounds.Valley); Navigation.Go("/game"); }
  render { Pressable("Play", onClick: Begin); }
}
```

### A loop replaces itself   {#loops}
`Sound.Loop(Sounds.Valley)` while `Valley` is already looping **stops the first one**. That is what you want: an app
that calls it from a render pass or on every state change would otherwise stack a fresh copy of the track on top of
the last one, over and over, with no way back short of reloading the page.

One consequence worth knowing: two independent loops of the *same* file are not expressible. Two loops of *different*
files are ordinary — each name is its own loop.

```osy syntax
Sound.Loop(Sounds.Engine);        // both play
Sound.Loop(Sounds.Wind);
Sound.Stop(Sounds.Engine);        // the wind keeps going
Sound.StopAll();                  // now nothing is looping
```

`Sound.StopAll()` stops loops only. A one-shot already in flight is milliseconds long and is left to finish — when
you reach for StopAll you mean "stop the music", not "cut the click that is half-played".

### Which formats, and why the file name does not decide   {#formats}
`.mp3`, `.wav`, `.ogg` and `.m4a`. The type a sound is served under is read from its **bytes** at compile time, never
from its extension — so a file whose name disagrees with its contents is a compile error naming both, rather than a
file served as something it is not.

Two rejections are worth knowing about because they look like they should work:

- An **`.mp4` holding video** is refused. A browser cannot decode it as audio, so accepting it would turn a
  wrong-file mistake into a sound that never plays and never says why.
- A **`.png` renamed to `.wav`** is refused, and so is any other image. Put a picture in `model/textures/`.

### One sound per name, and where the files live   {#one-per-name}
A sound name resolves to exactly one file. Two files with the same stem — `blip.wav` and `blip.mp3` — is an error
naming both, rather than a result that depends on which one the file system happened to list first.

The default glob is `**/sounds/*` at any depth, so `model/sounds/`, `model/pages/sounds/` and a kit's vendored
`ui/lib/sounds/` are all picked up. To put them somewhere else, declare the role in `app.osy`:

```osy syntax
app Arcade {
  model  "model/**/*.osy";
  sounds "audio/*.wav";        // instead of the default
}
```

### Size, and what is loaded when   {#limits}
A single sound may be up to **16 MB**, and one app's sounds up to 200 MB in total. Both are generous for their
purpose: an effect is a few kilobytes and a music track is a few megabytes.

Sounds under **1 MB are decoded when the page loads**, so the first effect you play is instant — a jump noise that
arrives 300 ms after the jump is worse than no jump noise. Larger files are fetched the first time you play them,
which is right for music: it starts once, and a few hundred milliseconds before it begins is inaudible.

### When nothing is heard   {#silence}
Silence is also what a correct app with no sound produces, so this surface says out loud what went wrong. Open the
browser console: every failure reports once, naming the sound and the reason — a name the app does not ship, a file
that would not fetch, a codec the browser would not decode, or audio the visitor has not unlocked yet.

## See also   {#see-also}
- [textures](https://osysharp.com/reference/ui/textures/) — the raster half of your app's assets, on the same file-is-the-declaration model
- [icons](https://osysharp.com/reference/ui/icons/) — single-colour glyphs
- [Canvas](https://osysharp.com/reference/ui/canvas/) — the drawing surface a game pairs sound with
- [component](https://osysharp.com/reference/ui/component/) — actions, `on frame`, and where these verbs are called from


---

<!-- https://osysharp.com/reference/ui/styling/ -->

# style props

> Inside a `variants` block, each `Name = value` is a style prop from a fixed vocabulary the renderer maps to CSS — paint (`Bg`, `Color`, `Border`), borders (`BorderW`, `BorderStyle`), spacing (`P`, `Gap`), size (`W`, `Grow`), and more. A value is a number, a keyword, or a theme token by name. NOT on this page: `align`, `justify` and `gap` are LAYOUT ARGUMENTS passed to `Stack`/`Row`/`Box`, not style props — `osy docs ui-layout` has all three.

<!-- id: ui-styling · area: ui · stability: stable · html: https://osysharp.com/reference/ui/styling/ -->

## Summary        {#summary}
A component's appearance lives in its **`variants`** block. Each leaf assignment there — `Bg = Surface;`,
`P = 4;`, `BorderW = 1;` — is a **style prop**: a name from a **fixed vocabulary** that the renderer maps to one or
more CSS properties. The vocabulary is closed and checked at compile time, so a misspelled prop (`Backgroud = …`) is
an error, not a declaration that silently styles nothing.

```osy syntax
component Card() {
  variants {
    base {
      Bg = Surface;         // paint
      P = 4;                // spacing (a step on the 0.25rem scale)
      Rounded = Card;       // a theme radius token
      BorderW = 1;          // a 1px border...
      Border = Border;      // ...painted with the theme's Border color
      Shadow = "0 1px 3px rgba(0,0,0,0.08)";   // a CSS box-shadow, or a theme Shadow token by name
    }
  }
  render { Stack { Slot; } }
}
```

> **Which token names do I actually have?** `osy kit --tokens` prints every token the starter theme ships — by
> group, with its value. (`osy model --json` reports the tokens your own `theme` blocks declare.) This page answers
> which style props exist; those answer which **values** they may take, and the two are different questions.

A style prop's **value** is one of four things: a **number** (lowered to the prop's unit — a spacing step, a pixel
length, or a raw number), a **keyword** from that prop's closed set (`Display = Display.Flex`), a **[theme tokens](https://osysharp.com/reference/ui/theming/) token**
referenced by name (`Bg = Surface`), or a **literal CSS string** for a prop whose value is passed through verbatim
(`Shadow`, `Cols`, …) — the per-prop tables below give each prop's value forms. A theme group is never a *prerequisite*:
an app with no `Shadow { }` group can still write `Shadow = "0 1px 3px rgba(0,0,0,0.08)"`. Reach for a token when the
value is part of the app's design language and should change with the theme; reach for a literal for a one-off.
Layout arguments (`align`, `justify`, `gap`) are separate — see [layout primitives](https://osysharp.com/reference/ui/layout/).

## Signature      {#signature}
```osy syntax
variants {
  base {
    <StyleProp> = <number | keyword | tokenName>;
    ...
  }
}
```

## Description    {#description}
The style-prop vocabulary is grouped by what it controls. Values are a number, a keyword (closed set), or a theme
token name.

### Which element a `variants` block styles — every top-level one   {#which-element}
A `variants` block styles the component's **render root**, and a component whose render has several top-level
elements has several roots. **All of them** wear the styling — whichever is on screen:

```osy title="both arms are roots, so both are styled" syntax
component Divider(bool vertical = false) {
  variants { base { Bg = Border; } }        // ← applies to whichever Box renders
  render {
    if (vertical)  { Box(w: "1px",  h: "100%"); }
    if (!vertical) { Box(w: "100%", h: "1px");  }
  }
}
```

That is the rule for every way an element can reach the top level: written there directly, inside an `if` arm, or
one per row of a top-level `foreach`. It is **not** inherited downward — anything nested inside a root is ordinary
content, and styles itself.

**A component whose render root is another component** passes the styling on: the child's own roots wear it, on top
of whatever the child's own `variants` block declares.

### What color is it — background, text, shadow, opacity   {#paint}

Every row below that says **color token** takes one of these — the starter theme's `Colors`, in scope wherever
`using Osysharp.Ui;` is:

`Colors.Bg` · `OnBg` · `Surface` · `OnSurface` · `Border` · `BorderDanger` · `Muted` · `TextMuted` ·
`TextSecondary` · `Primary` · `OnPrimary` · `Danger` · `Success` · `Warning` · `Scrim` · `Transparent`

`Primary`, `Danger`, `Success` and `Warning` are built with `Palette.From(...)`, so each is a whole RAMP rather
than one colour. A step is reached by a semantic alias — `Subtle` · `Muted` · `Default` · `Hover` · `Active` ·
`Strong` (`Bg = Colors.Primary.Hover`) — or by number: `50 · 100 · 200 · 300 · 400 · 500 · 600 · 700 · 800 · 900 ·
950` (`Colors.Primary[600]`). The plain name is the base.

An app that declares its own `theme` adds to this set rather than replacing it, so
**`osy model --json` reports the tokens THIS app actually has**, light and dark separately — the answer whenever
the list above is not the whole of it.

| Prop | CSS | Value |
|---|---|---|
| `Bg` | `background-color` | color token |
| `Color` | `color` | color token |
| `Border` | `border-color` | color token |
| `Rounded` | `border-radius` | length / radius token |
| `Shadow` | `box-shadow` | CSS value, or a shadow token — haloes the element's **rectangle** |
| `TextShadow` | `text-shadow` | CSS value — a shadow on the **glyphs**. A comma-separated list stacks, which is how a glow is built |
| `Opacity` | `opacity` | number |
| `CaretColor` | `caret-color` | color token — the text cursor in an input |
| `AccentColor` | `accent-color` | color token — a native checkbox/radio/range, themed without rebuilding it |
| `BgImage` | `background-image` | CSS value — a gradient or `url(…)`. A theme token reaches it as `"linear-gradient(…, var(--colors-accent), …)"`: every token *is* a custom property |
| `BgSize` | `background-size` | `Auto` · `Cover` · `Contain` |
| `BgPosition` | `background-position` | CSS value (`"center"`, `"50% 20%"`) |
| `Filter` | `filter` | CSS value (`"grayscale(1)"`, `"blur(2px)"`) |
| `BackdropFilter` | `backdrop-filter` | CSS value — blurs what is *behind* the box (a frosted modal scrim) |
| `MixBlendMode` | `mix-blend-mode` | `Normal` · `Multiply` · `Screen` · `Overlay` · `Darken` · `Lighten` · `ColorDodge` · `ColorBurn` · `HardLight` · `SoftLight` · `Difference` · `Exclusion` · `Hue` · `Saturation` · `Color` · `Luminosity` |

### Drawing a border, a divider, or a focus ring   {#borders}
`Border` (above) sets only the **color** — a border becomes visible once it has **width**. `BorderW` turns a solid,
theme-colored hairline **on**; `Border` recolors it. Per-edge widths draw a single rule — a rail's right edge, a
table row's bottom edge — without a full box.

| Prop | CSS | Value |
|---|---|---|
| `BorderW` | `border-width` | length (e.g. `1` → `1px`) |
| `BorderTW` | `border-top-width` | length |
| `BorderRW` | `border-right-width` | length |
| `BorderBW` | `border-bottom-width` | length |
| `BorderLW` | `border-left-width` | length |
| `BorderStyle` | `border-style` | `Solid` · `Dashed` · `Dotted` · `None` |

A hairline is `BorderW = 1;` (the style defaults to solid); recolor it with `Border = <token>`. A divider under a
list row is `BorderBW = 1;` on the row.

An **outline** is drawn outside the box and takes no layout space, which is what makes it the right tool for a focus
ring: showing one must not move the page.

| Prop | CSS | Value |
|---|---|---|
| `OutlineW` | `outline-width` | length (`2` → `2px`) |
| `OutlineColor` | `outline-color` | color token |
| `OutlineStyle` | `outline-style` | `Solid` · `Dashed` · `Dotted` · `None` |
| `OutlineOffset` | `outline-offset` | length — the gap between the box and the ring |

A custom focus ring is those four inside a `Focus { … }` block. **Only replace the default one, never remove it**:
`Outline*` with no visible result is a keyboard user losing their place on the page.

### Padding, margin, and the gap between children   {#spacing}

⚑ **The whole spacing and sizing vocabulary, stated compactly.** Measured on eval run 261: the model wanted a
full-viewport page, could not find `minH` in the style guide, wrote *"safer to skip it"*, and shipped a worse
layout for a prop that has always worked.

⚑ **Why a FENCE and not a paragraph.** `osy docs ui-styling` prints every code fence in full and replaces the prose
with a line saying how much there is — so a prop named only in a sentence is absent from the answer the command
actually gives, while a prop named in a fence is always in it. These props were prose, and that is why a run could
look them up and not find them.

```osy syntax
// SPACING — a number is a step on the 0.25rem scale (`p: 4` is 1rem); a string passes through (`mx: "auto"`).
Box(p: 4, px: 6, py: 2, pt: 1, pr: 1, pb: 1, pl: 1);      // padding
Box(m: 4, mx: "auto", my: 2, mt: 1, mr: 1, mb: 1, ml: 1); // margin
Stack(gap: 3);                                            // flex/grid gap — same scale, or a Space token

// SIZE — a number is px, a string passes through, or a Length token.
Stack(w: "100%", h: "4rem");
Stack(minW: "20rem", minH: Length.Screen);   // Length.Screen is how a page fills the viewport (100dvh)
Stack(maxW: "34rem", maxH: "40rem");   // maxW is how a column stops growing on a wide screen
Stack(w: Length.Measure);              // …or a Length token the kit ships — `osy kit --tokens` lists every one
// ⚠ THERE IS NO `Length.Column`. A page-column width is YOURS to name — declare it in your own theme first:
//      theme Default { Length { Column = "820px"; } }      // …then `Stack(maxW: Length.Column)` resolves
//   Without that declaration `Length.Column` is a compile error, not a fallback.
```

| Prop | CSS | Value |
|---|---|---|
| `P` `Px` `Py` `Pt` `Pr` `Pb` `Pl` | `padding` | a **number** is a step on the `0.25rem` scale — `p: 4` is `1rem`; a **string** passes through (`px: "auto"`) |
| `M` `Mx` `My` `Mt` `Mr` `Mb` `Ml` | `margin` | same scale; `mx: "auto"` is how a block centres |
| `Gap` | `gap` | the flex/grid gap, same `0.25rem` scale, or a `Space` token |

### How big is it — `W`/`H`, min/max, and flex grow   {#size}

| Prop | CSS | Value |
|---|---|---|
| `W` `H` | `width` `height` | a **number** → `px`, a **string** → as-is (`w: "100%"`), or a `Length` token |
| `MinW` `MinH` | `min-width` `min-height` | same. `minH: Length.Screen` is how a page fills the viewport — the kit token, `100dvh` under the hood, so a phone's address bar does not clip it |
| `MaxW` `MaxH` | `max-width` `max-height` | same. `maxW: "34rem"` is how a column stops growing on a wide screen |

…and the flex sizing trio:

| Prop | CSS | Value |
|---|---|---|
| `Grow` | `flex-grow` | number — `Grow = 1` makes a child fill the space its siblings leave |
| `Shrink` | `flex-shrink` | number — `Shrink = 1` lets a child shrink below a size you stated (see below) |
| `Basis` | `flex-basis` | length / size token — a child's starting size before grow and shrink apply |
| `AspectRatio` | `aspect-ratio` | a ratio (`"16 / 9"`, `"1"`) — reserves the box's shape before an image loads |

#### A size you state is a size you get   {#definite-size-holds}

**A definite size on the main axis holds.** `W` on a child of a `Row`, or `H` on a child of a `Stack`, is not a
suggestion that the layout may overrule: a child written `H = 2000` is 2000 tall, and a `Row` whose children total
more than its width overflows rather than squeezing them.

```osy syntax
Stack(h: 150, overflowY: Overflow.Auto) {
  Box(h: 2000) { Text("tall"); }     // 2000 tall — so the Stack scrolls
}
```

⚠ **THIS IS A DELIBERATE DEVIATION FROM CSS**, and the one place in the style vocabulary where a prop does not mean
exactly what its CSS twin means. In CSS `flex-shrink: 1` is the initial value, so an explicit `height` is only a
starting point and a flex child shrinks past it to fit. That default is right for a language where you write
`flex-basis` and think in flex terms; it is wrong for one where you write `h: 2000`, because writing a number *is*
the statement that you want that number.

**"Definite" means a plain length** — `150`, `12.5rem`, `50vh`. It does **not** include a percentage (which resolves
against the container, and is exactly where shrinking is the point), the content-driven keywords (`auto`,
`fit-content`, `min-content`, `max-content`), or anything computed or referenced (`calc(…)`, `min(…)`, `clamp(…)`,
a size token) — those could hold any of the above, so they keep the CSS default.

**To opt back in, say so:** `Shrink = 1` makes a child shrink again, and being an inline style it beats the rule,
so nothing is unreachable. Reach for it in the case it is meant for — a responsive toolbar whose items may compress.
Though the more usual answer there is not to state a width at all, and use `Grow` or `MinW = 0` instead.

⚑ **The cross axis is untouched.** `H` on a child of a `Row` never shrank, and still does not — `flex-shrink` only
governs the main axis.

### Styling text — font, weight, alignment, truncation   {#type}
| Prop | CSS | Value |
|---|---|---|
| `FontSize` | `font-size` | length / size token |
| `FontWeight` | `font-weight` | number (`600`) |
| `FontStyle` | `font-style` | `Normal` · `Italic` |
| `LineHeight` | `line-height` | number — **unitless** (`1.4`), so it scales with the font size |
| `LetterSpacing` | `letter-spacing` | length (`1` → `1px`) or a string (`"0.06em"`) |
| `FontFamily` | `font-family` | a string (`"Inter, system-ui, sans-serif"`) or a token |
| `TextAlign` | `text-align` | `Left` · `Center` · `Right` · `Justify` · `Start` · `End` |
| `TextTransform` | `text-transform` | `None` · `Uppercase` · `Lowercase` · `Capitalize` |
| `WhiteSpace` | `white-space` | `Normal` · `Nowrap` · `Pre` · `PreWrap` · `PreLine` |
| `FontVariant` | `font-variant-numeric` | `Normal` · `TabularNums` · `SlashedZero` · `OldstyleNums` |
| `TextDecoration` | `text-decoration` | `None` · `Underline` · `LineThrough` · `Overline` |
| `TextOverflow` | `text-overflow` | `Clip` · `Ellipsis` |
| `WordBreak` | `word-break` | `Normal` · `BreakAll` · `KeepAll` · `BreakWord` |
| `VerticalAlign` | `vertical-align` | `Baseline` · `Top` · `Middle` · `Bottom` · `Sub` · `Super` |

`WhiteSpace = WhiteSpace.Nowrap` keeps a tab, a table cell, or a button on one line (the row's own `OverflowX` handles the
excess). A tracked-out uppercase section label is `TextTransform = TextTransform.Uppercase; LetterSpacing = "0.06em";`; a numeric table
column right-aligns with `TextAlign = TextAlign.Right;`. `FontVariant = FontVariant.TabularNums` gives every digit the same width so a column
of numbers — a metric, a price, a count — lines up vertically instead of jittering as the digits change.

**A truncated line is three props, not one**: `TextOverflow = TextOverflow.Ellipsis` needs `WhiteSpace = WhiteSpace.Nowrap` and
`Overflow = Overflow.Hidden` beside it, or there is nothing to truncate. `WordBreak = WordBreak.BreakWord` is the other answer — for a
long URL or an unspaced identifier that would otherwise widen its container and push the whole layout sideways.

### Pinning, layering, scrolling and transforms   {#placement}
These are what let an app build a drawer, a sticky header, or a modal out of ordinary style props — the platform
widens this vocabulary rather than shipping the component.

| prop | CSS | values |
|---|---|---|
| `Position` | `position` | `Static` · `Relative` · `Absolute` · `Fixed` · `Sticky` |
| `Display` | `display` | `None` · `Block` · `Flex` · `Grid` · `InlineFlex` · `Contents` |
| `Overflow` | `overflow` | `Visible` · `Hidden` · `Auto` · `Scroll` |
| `OverflowX` | `overflow-x` | as `Overflow` — a table scrolls horizontally inside its card while the page owns the vertical scroll |
| `OverflowY` | `overflow-y` | as `Overflow` |
| `ScrollbarWidth` | `scrollbar-width` | `Auto` · `Thin` · `None` — so a tab strip scrolls without a chunky gutter |
| `ScrollBehavior` | `scroll-behavior` | `Auto` · `Smooth` — whether a programmatic scroll, from a bound `scrollTop` or a `scrollIntoView`, jumps or glides |
| `Inset` | `inset` | length — all four offsets at once |
| `Top` / `Right` / `Bottom` / `Left` | `top` / `right` / `bottom` / `left` | length |
| `Z` | `z-index` | a `ZIndex` token |
| `Transition` | `transition` | a motion token |

**Motion** is two props with a page of its own — [animation — looping motion with no destination state](https://osysharp.com/reference/ui/animation/) is where the

| prop | CSS | values |
|---|---|---|
| `Animation` | `animation` | names a declared `animation` block |
| `AnimationDelay` | `animation-delay` | duration — offsets one element's copy of it, which is how N elements running one animation become a chase rather than a lockstep |
| `FieldSizing` | `field-sizing` | `Fixed` · `Content` — an input that grows with what is typed into it |

Both are ordinary style props — usable inline or in a `variants` block — and [animation — looping motion with no destination state](https://osysharp.com/reference/ui/animation/) is where the
declaration, the timing vocabulary and the staggering example live.

**Transforms** move, turn and resize a box *without* re-running layout, so they composite on the GPU and cost nothing
per frame. That is why a drawer slides with `TranslateX` rather than by animating `Left`.

| Prop | CSS | Value |
|---|---|---|
| `TranslateX` | `transform: translateX(…)` | length or a string (`"-100%"`) |
| `TranslateY` | `transform: translateY(…)` | length or a string |
| `Rotate` | `rotate` | an angle (`"45deg"`) |
| `Scale` | `scale` | a number (`1.05`) or a pair (`"1.1 1"`) |
| `TransformOrigin` | `transform-origin` | the point it turns/scales about (`"center"`, `"top left"`) |

`TranslateX` and `TranslateY` both write the single `transform` property, so the compiler **merges** them into one
declaration — setting both is `transform: translateX(a) translateY(b)`, not one silently overwriting the other.
`Rotate`/`Scale` are CSS's own individual properties and compose on their own.

### Cursor, click-through, selection, hiding   {#interaction}
What a box does to the pointer, the caret and the selection — the props that make a decorative overlay click-through,
or a label un-selectable so a double-click selects the row instead of the word.

| Prop | CSS | Value |
|---|---|---|
| `Cursor` | `cursor` | `Auto` · `Default` · `Pointer` · `Text` · `Move` · `NotAllowed` · `Grab` · `Grabbing` · `Wait` · `Help` · `Crosshair` · `ColResize` · `RowResize` |
| `PointerEvents` | `pointer-events` | `Auto` · `None` — `None` makes a box invisible to the mouse; clicks pass through to whatever is beneath |
| `UserSelect` | `user-select` | `Auto` · `None` · `Text` · `All` |
| `Resize` | `resize` | `None` · `Both` · `Horizontal` · `Vertical` — a user-draggable textarea |
| `Visibility` | `visibility` | `Visible` · `Hidden` · `Collapse` — hidden but still occupying its space (unlike `Display = Display.None`) |
| `ObjectFit` | `object-fit` | `Fill` · `Contain` · `Cover` · `None` · `ScaleDown` — how an image fills its box |
| `AlignSelf` | `align-self` | `Auto` · `Start` · `Center` · `End` · `Stretch` · `Baseline` — one child opting out of the row's alignment |
| `Order` | `order` | number — reorders a flex/grid child visually **without** moving it in the DOM |

⚠ `Order` changes only the PAINTED order. Tab order and screen-reader order still follow the source, so a visual
order that disagrees with the document order is an accessibility bug, not a layout trick.

### Lining columns up across rows — grid   {#grid}
With `Display = Display.Grid`, these define a grid — the way to align columns across rows (a data table) without a shipped
Table component. Children flow into the tracks in order.

| Prop | CSS | Value |
|---|---|---|
| `Cols` | `grid-template-columns` | a track list (`"2fr 1fr 1fr"`, `"repeat(4, 1fr)"`, `"auto 1fr auto"`) |
| `Rows` | `grid-template-rows` | a track list |
| `ColSpan` | `grid-column` | how many columns a cell straddles (`"span 2"`, `"1 / -1"`) |
| `RowSpan` | `grid-row` | how many rows a cell straddles |
| `GridArea` | `grid-area` | a named area or an explicit span (`"1 / 1 / 3 / 2"`) |
| `GridAutoFlow` | `grid-auto-flow` | `Row` · `Column` — which way items that outrun the declared tracks flow |
| `GridAutoRows` | `grid-auto-rows` | the size of a row the track list did not declare (`"minmax(40px, auto)"`) |
| `GridAutoCols` | `grid-auto-columns` | the same, for columns |
| `Wrap` | `flex-wrap` | `Wrapping.Wrap` · `Wrapping.Nowrap` · `Wrapping.WrapReverse` — flex, not grid: whether a row breaks onto a second line. The vocabulary is `Wrapping`, so the prop and the value do not stutter |

A table header + rows all using the same `Cols` line up automatically; `Gap` (see [spacing](#spacing)) sets the grid gap. The
`GridAuto*` props take over when the data outruns the declared tracks, which is the usual case for a list of unknown
length: declare `Cols` and let the rows generate themselves.

### Styling hover, focus, disabled, and one screen width   {#states}
A nested `Hover { … }` / `Focus { … }` / `Active { … }` / `Disabled { … }` block styles that interaction state; a
nested breakpoint block (`Cozy { … }`) applies its props only at that width and up (mobile-first). Both nest inside
any variant value.

### Styling one atom without minting a component   {#inline}
The style props above are usually set in a component's `variants` recipe, but an atom can also carry them **inline** as
call arguments — the C#-natural way to style a one-off without minting a component for it:

```osy title="style a one-off atom without minting a component" syntax
Text("osyrin", fontSize: 17, fontWeight: FontWeight.Medium, color: Colors.TextPrimary)
Box(bg: Colors.Surface, p: 4, rounded: Radius.Card)
Icon(Icons.Chev, size: 18, color: Colors.TextMuted)
```

Argument names are the camelCase of the prop (`fontSize`, `bg`, `p`, `rounded`); values are the same forms as in a
variant (a theme token, a number, a string, a keyword) — **or a conditional** that picks between them (below). A
*hover* or *responsive* style still belongs in `variants` (those are pseudo-states and breakpoints, not value choices).

⚠ **Inline, a name value is written QUALIFIED — `bg: Colors.Surface`, not `bg: Surface`.** An argument slot accepts
every kind of value, so a bare capitalised name there could be a theme token, an enum member or a style keyword, and
all three are spelled alike; the group name is what says which vocabulary you meant. Each prop's group is the one its
values come from — `Colors` for `bg`/`color`/`border`, `Radius` for `rounded`, `Shadow` for `shadow`, `FontSize` /
`FontWeight` / `Font` for the type props, and the keyword props take their own name (`Display.Flex`,
`Position.Fixed`, `Overflow.Auto`, `Cursor.Pointer`). A bare name is refused, and the refusal names the exact
spelling to write.

⚑ **A `variants` block follows the same rule** — `Bg = Colors.Surface;`, `Rounded = Radius.Card;`,
`Display = Display.Flex;`. There is no position where a value is written bare. The prop on the left does *not*
decide the vocabulary, which is the thing that makes the rule uniform rather than arbitrary: any group's token is
accepted for any prop, so `Bg = Radius.Card;` is a legal (if odd) thing to write and the group is genuinely
carrying information.

The one sentence is: **a value names the vocabulary it comes from.** Not "when it is ambiguous" — always, so you
never have to work out which case you are in.

An inline value may be a **conditional** (`cond ? A : B`) whose branches are each an ordinary style value — so one
property can depend on one piece of component state without minting an enum + a `variants` dimension for it:

```osy title="inline style props, chosen per render" test app=ui-styling-inline
theme App {
  Colors { Surface2 = "#eef1f5"; TextPrimary = "#111827"; TextMuted = "#6b7280"; }
  Radius { Control = "8px"; }
}

[Composable] component Tab(string label, bool active, Action onClick) {
  render {
    Row(bg: active ? Colors.Surface2 : "transparent", color: active ? Colors.TextPrimary : Colors.TextMuted, rounded: Radius.Control) {
      Text(label);
    }
  }
}
```

The condition is a Boolean read from component state; the value re-evaluates and the element re-renders whenever that
state changes. Branches may mix a token and a string and may nest (`bg: a ? (b ? "#fee" : Colors.Border) : Colors.Bg`). For a whole
*set* of properties that changes together across several states, an enum-typed [state variant](#) is still the better
fit — a conditional value is for the common "one property, one condition" case.

### Giving an element a NAME — not a style prop   {#not-a-style-prop}
A style prop says how an element *looks*. Three other ambient vocabularies say what it **is**, what **state** it is in,
and what it is **called** — and the last one is the one people miss, because the obvious-looking candidate is a style
prop's neighbour in the same argument list:

```osy syntax
Input(value: email, placeholder: "you@example.com")            // ✗ has NO name
Text("Email", labelFor: field); Input(value: email, id: field) // ✓ named, and the words are clickable
```

A `placeholder:` is not a name: it disappears the moment you type, is not announced as the field's name, and nothing
can address the field by it. The naming props (`labelFor:`/`id:`, `labelledBy:`, `describedBy:`, `label:`) are ambient
— legal on every element, so they appear in no atom's signature. `osy kit --atoms` lists them under **Ambient**;
[accessibility](https://osysharp.com/reference/ui/accessibility/) is the full reference.

### Styling a child when an ANCESTOR is hovered — `inside`   {#inside}
A nested `inside <Component>.<State> { … }` block styles this element **when it sits inside** an ancestor component
that is in a given interaction state — the reveal-on-hover pattern (a row's actions, a card's menu, a tab's close):

```osy title="styling driven by an ANCESTOR's state" test app=ui-styling-inside
theme App {
  Motion { Fade = "opacity 0.12s ease"; }
}

[Composable] component RowActions() {
  variants {
    base {
      Opacity = 0; Transition = Motion.Fade;          // hidden by default…
      inside DataRow.Hover { Opacity = 1; }    // …shown when its DataRow is hovered
    }
  }
  render { Row { Text("⋯"); } }
}

[Composable] component DataRow() {
  render { Row { Slot; } }
}
```

The child names the ancestor, so it stays reusable in any matching container and the container needs to know nothing
about it. `<State>` is one of the universal interaction states (`Hover` / `Active` / `Focus`) — a *declared* state
like "selected" is data the child can see, so it rides an ordinary [state variant](#) fed by a prop, not `inside`.
The block nests inside a value block (usually `base`), like a pseudo-state.

## Examples       {#examples}

A flat, hairline-bordered card with a hover lift — the whole look from the vocabulary, no shipped component:

```osy title="a variant recipe" test app=ui-styling-recipe
theme App {
  Colors { Surface = "#ffffff"; Border = "#e5e7eb"; }
  Radius { Card = 12; }
}

[Composable] component Card() {
  variants {
    base {
      Bg = Colors.Surface; BorderW = 1; Border = Colors.Border; Rounded = Radius.Card; P = 4;
      Hover { BorderW = 1; Shadow = "0 1px 3px rgba(0,0,0,0.08)"; }
    }
  }
  render { Stack(gap: 2) { Slot; } }
}
```

A list row with a bottom divider, the last row suppressing it via a passed-in state:

```osy title="an enum dimension" test app=ui-styling-enum
theme App {
  Colors { Border = "#e5e7eb"; }
}

enum RowEdge { Divided, Last }

[Composable] component ListRow(RowEdge edge) {
  variants {
    base { Px = 3; Py = 2; }
    edge {
      Divided { BorderBW = 1; Border = Colors.Border; }
      Last { }
    }
  }
  render { Row(justify: Justify.Between) { Slot; } }
}
```

## See also       {#see-also}
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the design tokens a style prop's value references by name
- [layout primitives](https://osysharp.com/reference/ui/layout/) — `Stack`/`Row`/`Box` and the `align`/`justify`/`gap` layout arguments
- [component](https://osysharp.com/reference/ui/component/) — where a `variants` block lives and how a component is authored
- [accessibility](https://osysharp.com/reference/ui/accessibility/) — the other ambient argument vocabularies: what an element IS, what STATE it is in, and what it is CALLED


---

<!-- https://osysharp.com/reference/ui/control-styles/ -->

# styles — a control's own look knobs

> A `styles { }` block declares the look values a control owns — its paddings, widths, shadows — as named knobs an app can override. It is how a control exposes its geometry without turning every request into another prop, and without an app having to guess at CSS variables the control never promised.

<!-- id: ui-control-styles · area: ui · stability: stable · html: https://osysharp.com/reference/ui/control-styles/ -->

## Summary        {#summary}
A control reads the app's **theme** for colour, radius and type, so it looks like the rest of the app without being
told to. What a theme cannot say is anything about that control's own **geometry** — how much air its menu has, how
wide its popover opens, how heavy its shadow sits. Those are the control's business, and historically they were
literals inside it: an app could recolour the thing and not make it denser.

A **`styles { }`** block is the control's answer. It declares each knob with a kind, a name and a default; the app
overrides the ones it cares about and ignores the rest. Nothing changes unless the app says so.

The block is **optional**, and a control that declares none is unaffected in every direction.

## Signature      {#signature}
```osy syntax
control <Name> {
  styles {
    <kind> <KnobName> = <default>;
    …
  }
}
```

`<kind>` is one of `length` · `color` · `shadow` · `number` · `time` · `text`.

## Description    {#description}

### Declaring a control's style knobs   {#declaring}

```osy title="a grid that lets an app set its row height" test app=ui-control-styles
control DataGrid {
  contractVersion "1.1"
  participation headless
  props { string title; }
  styles {
    /// How tall one row is.
    length RowHeight = "2.25rem";
    color  HeaderBg  = Colors.Muted;
  }
}
```

**The kind is the value's SHAPE, not a CSS property.** `length RowHeight` says the knob holds a length; it does not
say what the control does with it. The platform deliberately never learns that a row has a height — it learns only
enough to check that an override is the right kind of thing. A control is free to use one knob in five places.

**A default is required.** A knob with no default is a hole the control has to write a fallback around, which is the
hand-maintained chain this block exists to remove.

**A default may reference a theme token**, and usually should:

```osy title="a default that stays a reference to the app's theme token" syntax
color HeaderBg = Colors.Muted;
```

That does not freeze the token's value at compile time — it stays a reference. So the control follows the app's
theme, dark mode included, until the app overrides the knob specifically. This is how a control ships a considered
look without shipping a look **of its own**.

### Reading them in the shim    {#reading}

The generated `.d.ts` gives the control a typed scope keyed by its own declared names:

```ts
export interface DataGridHost {
  tokens: TokenScope;                                  // the APP's design language
  styles: StyleScope<"RowHeight" | "HeaderBg">;        // THIS control's knobs
}
```

so the shim asks for a knob by the name it declared:

```ts
el.style.height = host.styles.cssVar('RowHeight');     // "var(--control-datagrid-rowheight, 2.25rem)"
```

The union is the point: a misspelled knob is a compile error in the shim, where a misspelled CSS custom property
would simply resolve to nothing and paint as though the value were absent.

Use **`cssVar`** in a style so an override stays live without a re-mount. Use **`get`** only for a decision in
JavaScript — it reads the value now, and a value read once does not follow a later change.

An **`opaque`** control cannot declare a `styles { }` block at all. An island that paints its own way is handed no
style scope, so its knobs would have no reader — while still looking, from the app's side, like a surface it could
theme. Declaring one is a compile error rather than a block that quietly does nothing.

### Overriding, app-wide    {#overriding-app-wide}

A theme's `Control` block sets a knob for every instance in the app:

```osy title="the app decides its grid is roomier" test app=ui-control-styles-theme
control DataGrid {
  contractVersion "1.1"
  participation headless
  props { string title; }
  styles { length RowHeight = "2.25rem"; }
}

theme Doc {
  Control {
    DataGrid { RowHeight = "3rem"; }
  }
}
```

Both names are **checked against the declaration**. A control the app does not have, or a knob that control never
declared, is a compile error that lists what it does declare — not a value that quietly applies to nothing.

### Overriding, per call site    {#overriding-per-call}

This is the tier a theme cannot express: two instances of the same control, on the same page, styled differently —
a dense grid in a sidebar and a roomy one in the main column.

```osy title="one grid roomy, one left alone" test app=ui-control-styles-callsite
control DataGrid {
  contractVersion "1.1"
  participation headless
  props { string title; }
  styles {
    length RowHeight = "2.25rem";
    color  HeaderBg  = Colors.Muted;
  }
}

theme Doc { Colors { Muted = "#F0EEE8"; Surface = "#FFFFFF"; } }

[Page("/orders")] [AllowAnonymous]
component OrdersPage() {
  render {
    Stack {
      DataGrid(title: "Orders", styles: new() { RowHeight = "3rem", HeaderBg = Colors.Surface });
      DataGrid(title: "Recent");
    }
  }
}
```

The second call is untouched — an override applies to the instance that wrote it, and a knob it says nothing about
keeps whatever the tiers below give it. An override is not a reset.

Names are checked here exactly as they are in a theme: a knob this control never declared is a compile error listing
the ones it does declare.

### What a style value may be    {#style-values}

A literal or a **theme token**, in every one of the three places a knob's value is written — the control's default,
the theme override, and the call site:

```osy syntax
styles: new() { RowHeight = "3rem", HeaderBg = Colors.Surface }
```

A token stays a **reference**, so an overridden knob still follows the app's theme and its dark mode.

Nothing else, and that is deliberate rather than a gap. A style value is written into CSS **once** — there is no
channel that re-evaluates it while the page is mounted — so an expression that could change would silently paint
whatever it happened to be. An app that wants a value to vary declares a theme token and references it; the cascade
already follows that.

### Which override wins — precedence   {#precedence}

Ordinary CSS cascade, in the ordinary direction:

| | |
|---|---|
| the control's **default** | the `var()` fallback — lowest |
| the app's **theme** override | a declaration in the app's stylesheet |
| the **call-site** override | inline on the instance — highest |

Nothing arbitrates this and nothing asks "did the app override it?". The shim asks for the variable and paints with
whatever came back.

### Choosing between a style, a prop and a token    {#choosing}

| Use | When |
|---|---|
| a **theme token** | the value is the app's design language and every control should read it — a brand colour, a radius scale |
| a **`styles` knob** | the value is this control's own geometry, and an app might reasonably want it different |
| a **prop** | it changes BEHAVIOUR, or it is a small closed set of choices the call site makes (`density`, `readOnly`) |

The test that separates the last two: if changing it would change what the control DOES, it is a prop. If it only
changes how the same thing looks, it is a style.

## Examples       {#examples}

A control whose knobs default to the app's theme, with the app overriding one of them:

```osy title="inherits the theme, then one knob is claimed" test app=ui-control-styles-full
control Callout {
  contractVersion "1.1"
  participation headless
  props { string text; }
  styles {
    length Pad     = "0.75rem";
    color  Surface = Colors.Muted;
    color  Accent  = Colors.Primary;
  }
}

theme Doc {
  Colors { Primary = "#4F46E5"; Muted = "#F0EEE8"; }
  Control {
    Callout { Pad = "1.25rem"; }
  }
}
```

`Surface` and `Accent` keep following the theme; only `Pad` is claimed by the app.

## See also       {#see-also}
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the `control` block these are declared in
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the app's own design tokens, which a default may reference
- [style props](https://osysharp.com/reference/ui/styling/) — how style values are written elsewhere in the UI surface


---

<!-- https://osysharp.com/reference/ui/textures/ -->

# textures

> Drop `.png`, `.jpg` or `.webp` files into `model/textures/` and blit them onto a canvas with `Draw.Image(wall, …)`. The name is checked at compile time, so a typo is an error rather than a blank sprite. Textures are the raster half of your app's art — the vector half is `Svg(name)`, and a single-colour glyph is `Icon(name)`.

<!-- id: ui-textures · area: ui · stability: preview · html: https://osysharp.com/reference/ui/textures/ -->

## Summary        {#summary}
A texture is a **file in your app**, not data. Put a `.png` in `model/textures/` and it becomes part of your app's
vocabulary:

```text
model/
  textures/
    wall.png
    floor.png
    sprites.png
```

`wall.png` is now drawable as `Draw.Image(wall, …)`. There is nothing to register and nothing to import.

Textures are the **third kind of art** an app can ship, and they divide by what the picture *is* rather than by
taste:

| you have | put it in | draw it with |
|---|---|---|
| a single-colour glyph that should follow your text | `model/icons/` | `Icon(Icons.Search)` |
| a vector illustration, logo or background | `model/art/` | `Svg(Art.Hexgrid)` |
| a **bitmap** — a wall texture, a sprite sheet, a photograph | `model/textures/` | `Draw.Image(wall, …)` |

## Signature      {#signature}
```osy syntax
Draw.Image(wall, dx, dy, dw, dh)                          // the whole texture, into a destination rectangle
Draw.Image(wall, sx, sy, sw, sh, dx, dy, dw, dh)          // a SOURCE rectangle of it, into a destination one
Draw.Image(url, …)                                        // the same two forms, over a runtime url
```

## Description    {#description}

### The name is checked   {#names}
`Draw.Image(wall, …)` names the texture by a **bare identifier**, checked against the textures your app actually
ships. A typo is a compile error that lists what there is:

```text
`Draw.Image` takes a declared texture or a url — 'walll' is neither a variable in scope nor a
declared texture (declared: floor, wall). Did you mean 'wall'?
```

Because the name is an identifier, a texture's **file name must be one too** — `brick_wall.png`, not
`brick-wall.png`. A kebab-case file is rejected with the rename to make.

### A url still works, and a collision is refused   {#url-form}
Unlike `Icon` and `Svg`, this first argument can legitimately be an **expression**: a url is genuinely a runtime
value sometimes — a user's uploaded avatar, a signed download link.

```osy title="a url decided at runtime" test app=arcade-avatar
[Page("/avatar")]
[AllowAnonymous]
component Avatar() {
  string src = "/uploads/me.png";

  on frame (double dt) {
    Draw.Clear("#111");
    Draw.Image(src, 8, 8, 64, 64);       // an expression — the url form
  }

  render { Canvas(w: 80, h: 80); }
}
```

The texture form is the same call with a **declared name** in that first position — and both argument forms in one
page, which is the whole surface:

```osy title="a tiled wall, and one tile magnified" test app=arcade
[Page("/wall")]
[AllowAnonymous]
component Wall() {
  on frame (double dt) {
    Draw.Clear("#1a1a22");
    Draw.Image(Textures.Brick1, 0, 0, 32, 32);                 // the WHOLE texture, into a destination rectangle
    Draw.Image(Textures.Brick1, 0, 0, 4, 4, 40, 0, 64, 64);    // a SOURCE rectangle of it, into a destination one
  }

  render { Canvas(w: 112, h: 64); }
}
```

That example is compiled on every docs build **against a real image file** — `canvas-texture` is a complete sample
app shipping `textures/wall.png`, and `osy docs sample canvas-texture` hands you the whole thing.

So a bare identifier could mean either, and when it means **both** the compiler refuses rather than picking:

```text
`Draw.Image(wall, …)` is ambiguous — 'wall' is both a declared texture and a variable in scope,
and the two draw different things. Rename one of them.
```

Every precedence rule here would produce a silent bug in one direction or the other — a texture drawn where a
variable was meant, or a local added months later quietly changing what a call site draws. Renaming one of the two
costs seconds; finding either of those costs an afternoon.

### Which formats, and why the file name does not decide   {#formats}
`.png`, `.jpg`/`.jpeg` and `.webp` — the three raster formats a browser decodes into a canvas.

The type a texture is **served** under is read from its **bytes**, never from its extension. A consequence worth
knowing: a file whose name disagrees with its contents is a compile error rather than a quiet re-label.

```text
'wall.png' is really a JPEG, whatever its extension says — rename it to 'wall.jpg'.
```

An `.svg` in `model/textures/` is **not** a texture. A vector image is markup that has to be sanitized before it
reaches a page, which is what `model/art/` and `Svg(name)` are for.

### Sampling a source rectangle   {#source-rect}
The nine-argument form takes a rectangle **of the texture** and scales it into a rectangle **of the canvas**. It is
what a sprite sheet needs, and what a textured raycaster needs — a one-pixel-wide column of the texture stretched
to a wall's height:

```osy syntax
// one screen column: texel column `texX` of a 64x64 texture, over the wall's full height
Draw.Image(wall, texX, 0, 1, 64, x, top, colWidth, wallHeight);
```

### Shading a texture   {#shading}
A blit paints the texture's own pixels, so lighting is a second pass over the top rather than a colour argument —
draw the texture, then wash it with a translucent rectangle:

```osy syntax
Draw.Image(wall, texX, 0, 1, 64, x, top, colWidth, wallHeight);
Draw.Rect(x, top, colWidth, wallHeight, "rgba(0,0,0,0.35)");   // distance falloff
```

### One texture per name   {#one-per-name}
Two files claiming the same stem is an error naming both, because a name has to resolve to one file. The same rule
applies when a kit vendors its textures into your tree.

### Where texture files live, and how to move them   {#where}
The default is any `textures/` folder in your source tree. Declare the role in `app.osy` to put them somewhere
else:

```osy syntax
app Arcade {
  model    "model/**/*.osy";
  textures "assets/textures/*.png";
}
```

### Reading a texture's pixels   {#reading}
`Texture.Pixels(wall)` answers the texture's pixels as a `List<int>` — one packed `0xRRGGBB` colour per pixel, in
row order — so a software renderer can sample it. `Texture.Width(wall)` and `Texture.Height(wall)` give its size.

It answers the **whole buffer**, once, rather than a texel at a time, and that is a performance contract rather
than a convenience: a call costs roughly seven times an arithmetic operation, so asking per texel would cap a
per-pixel effect at a few thousand pixels a frame before any of its own work. Read it into a field, index it in the
loop.

> ⚠ A texture is decoded by the browser, so `Texture.Pixels` answers an **empty list** until it has been — the same
> "one frame away" rule `Draw.Image` follows. Read it in the frame body until it arrives, not in `on mount`, which
> runs once and would lose the race permanently.

```osy syntax
List<int> texels = new List<int>();

on frame (double dt) {
  if (texels.Count == 0) { texels = Texture.Pixels(wall); }
  // …now index it: texels[Texture.Width(wall) * y + x]
}
```

Colours are 24-bit RGB, not 32-bit ARGB, because an Osy# `int` is a signed 32-bit integer — any alpha above `0x7F`
would overflow it and hand you negative colours. A pixel buffer is opaque; transparency is expressed by not
drawing.

### Size, and the limits   {#limits}
A texture's **intrinsic size is read at compile time**, from the image header — so it is known before anything
decodes, and a header claiming an absurd size is refused there rather than becoming a very large canvas later. A
texture may be at most 8192×8192 and 8 MB.

### Passing a texture around — `Textures` is a type   {#as-a-value}
`Textures` is a type, so a texture is a value you can pass and store like any other. A helper that blits one takes
it as a parameter:

```osy syntax
void Blit(Textures tex, int x) {
  Draw.Image(tex, x, 0, 128, 128);
}

// at the call site
Blit(Textures.Wall1, 0);
```

The same holds for a return type, a local, and a field on an entity. Because a stored texture is keyed by the file's
**name** rather than by a position in a list, adding a new file never changes what an already-stored row means.

## See also   {#see-also}
- [Canvas](https://osysharp.com/reference/ui/canvas/) — the surface `Draw.Image` paints on, `Draw.Pixels` for a whole pixel buffer, and the rest of the `Draw.*` vocabulary.
- [icons](https://osysharp.com/reference/ui/icons/) — the single-colour glyph vocabulary, and the closest analogue to how a texture is named.
- [component](https://osysharp.com/reference/ui/component/) — where an `on frame` body lives.


---

<!-- https://osysharp.com/reference/ui/theming/ -->

# theme tokens

> A `theme` block names your app's design tokens — colors, spacing, radii, and more — as reusable values. A token can hold a literal value (`Primary = "#0077B6"`) or reference another token by name (`Grid = Border`), so shared values stay defined in one place.

<!-- id: ui-theming · area: ui · stability: stable · html: https://osysharp.com/reference/ui/theming/ -->

## Summary        {#summary}
A **`theme`** block declares your app's **design tokens** — the named values (colors, spacing steps, radii, …)
your UI is built from. Grouping them in a theme means a value like your brand color is defined **once** and reused
everywhere, so restyling the app is a change in one place.

`osy kit --tokens` prints the tokens you already have — the starter theme the [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) ships, by group and with
each value — which is where to look before inventing a name.

```osy title="tokens, grouped" test app=ui-theming
theme Default {
  Colors {
    Primary = "#0077B6";
    OnPrimary = "#FFFFFF";
    Border = "#E3E6EA";
  }
  Radius {
    Md = 10;
    Lg = 16;
  }
}
```

Tokens are organized into **groups**. Each leaf assignment (`Primary = "#0077B6"`) is one token; its **path**
through the groups (`Colors` → `Primary`) is what makes its name unique, so two groups can both have a `Md` without
colliding.

The top-level groups are a **closed set** — the token *kinds* the editor understands, so `Bg = ‹caret›` can offer
your colors, `FontSize = ‹caret›` your sizes, and so on:

> `Colors` · `Space` · `Radius` · `Font` · `FontSize` · `FontWeight` · `Shadow` · `Motion` · `ZIndex` ·
> `Breakpoints` · `Series` · `Length` · `Density` · `Touch`

The font kinds are **separate flat groups** (`Font` for families, `FontSize`, `FontWeight`) rather than one mixed
`Type { … }`, so each token's kind is unambiguous. An unrecognized group name is a **compile error** with a
did-you-mean — a theme group that isn't one of these is a typo, not a silent no-op.

## Signature      {#signature}
```osy syntax
theme { Colors { … } Radius { … } } — named design tokens, and references between them
```

## Description    {#description}

### Where a theme goes — and why there is nothing to wire   {#where}
**Declaring it is the whole step.** Put the `theme` block in any file the manifest's `model` glob already covers —
`model/theme.osy` by convention — and every control and page in the app is styled by it. There is no registration
call, no `Theme =` on the app, nothing to import: a theme is part of the model, like an entity.

The **name is only a name.** `theme Default` and `theme Anything` behave identically; light and dark are a property
of each TOKEN (`Modes.Of(light: …, dark: …)` — see [[ui-theming#modes]]), not of the block.

*(Said here because its absence is what a reader goes looking for. Every other framework has a provider, a plugin or
a config key, so "I have written the tokens — now how do I attach them?" is the next question, and a page that never
answers it reads as incomplete. One run spent a call grepping this page for `app.osy`, `Theme =`, `UseTheme` and
`attach`, and found nothing, because there is nothing.)*

### The groups a theme may declare   {#groups}
A theme's top-level groups are a **closed set** — anything else is a compile error with a did-you-mean. Each group
feeds a family of style props, which is why the grouping exists rather than one flat list of names.

| Group | Holds | Feeds |
|---|---|---|
| `Colors` | colours and palettes | `bg:`, `color:`, `border:` |
| `Space` | the few spacings that are a decision, not a scale step | `p:`, `m:`, `gap:` |
| `Radius` | corner radii — owns the `Sm`/`Md`/`Lg` triple | `rounded:` |
| `Font` | font stacks | `font:` |
| `FontSize` | type sizes, named for the ROLE the text plays | `fontSize:` |
| `FontWeight` | type weights | `fontWeight:` |
| `Shadow` | elevation, named for the elevation not the control | `shadow:` |
| `Motion` | whole transitions, not bare durations | `transition:` |
| `ZIndex` | layering policy | `z:` |
| `Breakpoints` | your own width names — `Layout.AtLeast(Tablet)` reads these | responsive props |
| `Length` | widths, heights, control and row heights | `w:`, `h:`, `minW:`, `maxW:`, `basis:` |
| `Series` | chart series colours | plotting controls |

`Length` is the sizing group. `Size`, `Density` and `Touch` are accepted as named sub-scales of the same kind, so an
existing theme keeps compiling — but the kit spends `Size` on the control-size **enum** (`size: Size.Lg`), and one
spelling standing for two different things is the reason `Length` is what every kit example uses.

### Giving a token a plain value — a color or a number   {#literals}
A token most often holds a **literal** value — a color string or a number:

```osy title="literal tokens" test app=ui-theming
theme Default {
  Colors   { Primary = "#0077B6"; }
  Radius   { Md = 10; }
  FontSize { Body = "13px"; Heading = "22px"; }
}
```

Each literal token becomes a reusable style value your components draw from. Changing the literal changes every
place that uses the token.

### Can one token reuse another's value?   {#references}
A token can reference **another token by name** instead of repeating a value:

```osy title="one token referencing another" test app=ui-theming
theme Default {
  Colors {
    Border = "#E3E6EA";
    Grid   = Colors.Border;      // Grid resolves to whatever Border is
  }
}
```

`Grid = Border` keeps `Grid` pointing at `Border` as a **living reference**, not a copy — if you later change
`Border`, `Grid` follows automatically, with no need to update it or rebuild. A reference names a token by its
plain leaf name; a token in the **same group** is preferred, so the `Border` above binds to the `Border` in
`Colors`.

Referencing a token that doesn't exist is a **compile error** — a typo like `Grid = Bordr` is caught, not
silently ignored (the same way a mistyped color step like `Primary.Hund` is).

### One seed color, a whole ramp — `Palette.From`   {#palettes}
A color token can be a whole **palette** instead of a single value — `Primary = Palette.From("#0077B6")` generates an
even ramp of shades from one seed, and `Primary.Hover` / `Primary[600]` reach its steps. See [color palettes](https://osysharp.com/reference/ui/palette/) for the
full story.

```osy title="a token that is a whole ramp" test app=ui-theming
theme Brand {
  Colors {
    Primary = Palette.From("#0077B6");
    Line    = Primary[200];        // a light step from the ramp
  }
}
```

### Why is my token name a collision? — one leaf, one value   {#unique-names}
A reference names a token by its **group and name** (`Rounded = Radius.Card`) — a bare leaf does not say which
vocabulary it means, since a theme token, an enum member and a style keyword are all spelled alike in that
position. The leaf still has to denote exactly **one** value across your whole app, because the token map is keyed
by it: declaring the same name under two different groups is a compile error:

```osy title="✗ one leaf name under two groups is a collision" syntax
theme Admin {
  Space  { Md = "12px"; }
  Radius { Md = "8px"; }     // error: duplicate token 'Md' declared in groups 'Space' and 'Radius'
}
```

Name tokens by their **role** and the question doesn't arise — `Radius { Control; Card; Pill; }` reads better at
the call site (`Rounded = Card`) than a second `Sm/Md/Lg` scale would, and it can only mean one thing.

Re-declaring a name **under the same group** is not a collision — that is how you override a token a UI kit
shipped, and both resolve to the same value slot.

**Your value wins.** A token you declare **shadows** a same-named one from the kit, exactly as your own component
shadows a kit component of the same name. So a theme of your own needs to restate only what you are changing:

⚑ That holds across GROUPS too, and it is why declaring `FontWeight { Normal = …; }` is not an error even though
the kit ships a `Motion.Normal`: leaf names are one flat namespace, so your declaration takes the leaf and the
kit's becomes unreachable in your app. A use site that names the kit's group is told so —
*"'Normal' is a token in the `FontWeight` group, not `Motion`"* — rather than the whole theme being refused.

```osy title="✓ restate only the kit token you are changing" syntax
theme Brand {
  Colors { Primary = "#0077B6"; }   // shadows the kit's Primary
}
// every other kit token — Surface, Border, Danger — still applies
```

Shadowing is per **token**, not per theme: the kit's other tokens keep applying, so you never have to copy a kit's
whole palette to change one colour of it.

### Does a length token need `px`? — units   {#units}
A token that holds a **length** carries its own unit, as a string:

```osy title="a length carries its unit" test app=ui-theming-units
theme Admin {
  Radius { Card = "12px"; }      // ✓  border-radius: 12px
  Space  { Md = "12px"; }        // ✓  gap: 12px
}
```

A bare number is emitted **unitless**, which is correct for `ZIndex`, `FontWeight` and `Breakpoints` — and wrong
for anything the browser needs a unit for. Style props are the other way round: there a bare number takes the
prop's unit (`Px = 4` → `1rem`), because the prop already knows what it is.

### Dark mode — per-mode values   {#modes}
A token can hold **different values per mode** — most commonly light and dark — by giving it a mode map:

```osy title="one token, two modes" test app=ui-theming
theme Default {
  Colors {
    Surface = Modes.Of(light: "#FFFFFF", dark: "#111111");
    OnSurface = Modes.Of(light: "#111111", dark: "#F5F5F5");
  }
}
```

The **`Light`** value is the default. **`Dark`** applies automatically when the visitor's device prefers a dark
color scheme — the correct colors are there on the very first paint, with **no flash**. You can also force a mode
explicitly (for a theme toggle) by setting `data-theme="dark"` on the page, which wins over the device preference.
A visitor's explicit choice is **remembered across visits** and applied on the first paint of their next visit —
still with no flash — so a returning user always lands in the mode they picked.

A mode value is a normal token value — a literal (as above) or a reference to another token — so everything from
[the sections above](#references) applies inside a mode map too. Modes are open-ended: `Light` and `Dark` are the
common pair, but you can define others and select them with `data-theme`.

### Making the app dark-only (or light-only) — `Mode`   {#mode}
The device preference is the right default for an app that offers **both** looks. An app that has **one** look says
so, with a `Mode` setting at the top of its theme:

```osy title="this app is dark, full stop" test app=ui-theming-mode
theme Midnight {
  Mode = Dark;
  Colors {
    Surface = Modes.Of(light: "#FFFFFF", dark: "#111111");
    Accent  = "#FF2D95";
  }
}
```

Now every token resolves its **`Dark`** value by default, on any device, on the first paint. `Mode = Light;` does the
same in the other direction — worth writing when your app is deliberately light, because without it a visitor whose
phone prefers dark gets the dark column of every mode map you wrote.

It pins the **default**, not the choice: `data-theme` still wins, so [a toggle](#toggle) keeps working in an app that
declares a `Mode`. Leave `Mode` out to follow the device, which is what every theme does by default.

This matters most for **tokens you did not write**. The UI kit's controls paint
with the kit's own `Surface`/`OnSurface`/`Border` tokens — and those carry mode maps. So an app whose own palette is
dark, but which never says `Mode = Dark;`, gets kit buttons and cards in their **light** colors on a light-mode
browser: a white button on a near-black page, from source that reads perfectly.

### Switching mode — a toggle   {#toggle}
Two verbs switch the mode at runtime and **remember the choice**:

- **`Theme.Toggle()`** — flip between light and dark.
- **`Theme.Set(mode)`** — apply a named mode, e.g. `Theme.Set("dark")` (use this when your theme has more than the
  light/dark pair).

Both persist the choice, so it survives navigation and the visitor's next visit (applied on the first paint, no
flash). Call them from an action:

```osy title="your own toggle" test app=ui-theming-toggle
component ModeButton() {
  action Flip() { Theme.Toggle(); }
  render { Pressable(onClick: Flip) { Text("🌓"); } }
}
```

You don't have to write your own — the UI kit ships a ready one. Drop **`ThemeToggle()`**
into any page (pass `label` to change its face, or declare your own same-named `ThemeToggle` to fully restyle it):

```osy title="the kit's ready-made one" test app=ui-theming-toggle
[Page("/")]
[AllowAnonymous]
component Home() {
  render {
    Row { Text("My app"); ThemeToggle(); }
  }
}
```

The platform ships the switching **mechanism** and the kit control, but injects no toggle of its own — where the
button lives is your layout's decision, not the platform's.

### Styling a component — `variants`   {#style-props}
A component styles itself with a `variants` recipe. `base` is what it always looks like; each **dimension** names one
of the component's parameters, and adds the styling that applies for its value.

```osy title="an enum dimension" test app=ui-theming-variants
theme Kit {
  Colors { Surface = "#FFFFFF"; OnSurface = "#111111"; Primary = "#0077B6"; OnPrimary = "#FFFFFF"; Danger = "#C1121F"; }
  Radius { Md = "10px"; }
}

enum Tone { Neutral, Primary, Danger }
enum Size { Sm, Md, Lg }

component Button(Tone tone, Size size) {
  variants {
    base { Bg = Colors.Surface; Color = Colors.OnSurface; Rounded = Radius.Md; Px = 4; Py = 2; }
    tone { Primary { Bg = Colors.Primary; Color = Colors.OnPrimary; } Danger { Bg = Colors.Danger; } }
    size { Sm { Px = 3; Py = 1; } Lg { Px = 5; Py = 3; } }
  }
  render { Text("Save"); }
}
```

An **enum** parameter lists a block per member, as above. A **bool** parameter has only one thing to say, so it says
it directly — these are the styles that apply when it is true:

```osy title="a bool dimension" test app=ui-theming-variants
component Rail(bool collapsed, bool drawerOpen) {
  variants {
    base { W = "264px"; }
    collapsed { W = "64px"; }        // when `collapsed` is true
    drawerOpen { TranslateX = "0"; } // when `drawerOpen` is true
  }
  render { Text("Rail"); }
}
```

There is no `false` block, because not applying the styles is exactly what false means. Writing the enum shape on a
bool (`collapsed { True { … } }`) is a compile error that shows you the spelling above.

Reach for a bool before minting an enum to carry one. A two-member `enum RailMode { Open, Collapsed }` says no more
than `bool collapsed` does, and it costs you at the call site: the component ends up taking the enum *and* a bool for
the same fact, because a variant's enum value is not something the render body can read back as a condition.

The property names are a **closed vocabulary** — a typo is a compile error with a suggestion, not a declaration
that silently does nothing:

| Group | Props |
|---|---|
| Paint | `Bg` `Color` `Border` `Rounded` `Shadow` `Opacity` |
| Spacing | `P` `Px` `Py` `Pt` `Pr` `Pb` `Pl` · `M` `Mx` `My` `Mt` `Mr` `Mb` `Ml` · `Gap` |
| Size | `W` `H` `MinW` `MinH` `MaxW` `MaxH` `Grow` |
| Type | `FontSize` `FontWeight` |
| Placement | `Position` `Display` `Overflow` `Inset` `Top` `Right` `Bottom` `Left` `Z` |
| Motion | `Transition` `TranslateX` `TranslateY` |

**Values are tokens, numbers, or keywords.** A bare name is a [token reference](#references) (`Bg = Surface`) —
which is what lets one theme restyle everything. A number takes the prop's unit: spacing props use the spacing
scale (`Px = 4` → `1rem`), size props are pixels (`W = 280` → `280px`).

`Position`, `Display` and `Overflow` take a **keyword** from a fixed set, because there is nothing a theme could
usefully say about `position: fixed`. A keyword is written **qualified**, with the prop's own name as the group —
`Position = Position.Fixed;` — because a bare `Fixed` could equally be a token you declared, and the two are spelled
alike. (A token reference stays bare, as above: the prop already decides that vocabulary.)

| Prop | Accepts |
|---|---|
| `Position` | `Position.Static` `Position.Relative` `Position.Absolute` `Position.Fixed` `Position.Sticky` |
| `Display` | `Display.None` `Display.Block` `Display.Flex` `Display.Grid` `Display.InlineFlex` `Display.Contents` |
| `Overflow` | `Overflow.Visible` `Overflow.Hidden` `Overflow.Auto` `Overflow.Scroll` |

### Responsive — one design, every width   {#responsive}
Declare your breakpoints as tokens, then override any style prop at any of them:

```osy title="a breakpoint override" test app=ui-theming-responsive
theme Admin {
  Breakpoints { Compact = 640; Cozy = 960; }
}

enum RailState { Closed, Open }

component Sidebar(RailState rail) {
  variants {
    base {
      Position = Position.Fixed; TranslateX = "-100%";     // a phone: an off-screen drawer
      Cozy { Position = Position.Static; TranslateX = "0"; }   // ≥ 960px: a docked rail
    }
    rail { Open { TranslateX = "0"; } }
  }
  render { Text("Sidebar"); }
}
```

Styling is **mobile-first**. `base` is unconditional — it is what the narrowest screen gets — and each breakpoint
**adds** styling on top of it as the screen grows. Write it this way round and a phone never pays to undo a desktop
layout it was never going to use.

A breakpoint block may contain any style props, and may nest a pseudo-state (`Cozy { Hover { … } }`). Naming a
breakpoint you didn't declare is a compile error, with a suggestion.

**Which width?** For a component, the browser window. A component decides *placement* — is this a sidebar or a
drawer — and placement is a property of the page, not of the box the component happens to sit in. A **foreign
control** is the opposite: it reflows its own interior against the size of its own mount element, so the same grid
becomes cards whether it is narrow because the phone is narrow or because you put it in a narrow panel. Your
breakpoint tokens are visible to controls as CSS variables, so both use the same numbers.

### There is no drawer or modal — building chrome from tokens   {#chrome}
The platform ships no drawer, no modal, no sticky header. It ships the vocabulary, so you build the one you want —
and the theme still owns the **policy**: which layer things stack on, how fast they move.

```osy title="layering and timing as tokens" test app=ui-theming-chrome
theme Admin {
  ZIndex { Overlay = 20; }
  Motion { Slide = "transform 0.18s ease"; }
}

enum RailState { Closed, Open }

component Sidebar(RailState rail) {
  variants {
    base { Position = Position.Fixed; Inset = 0; Right = 0; W = 280; Overflow = Overflow.Auto;
           Z = ZIndex.Overlay; Transition = Motion.Slide; TranslateX = "-100%"; }
    rail { Open { TranslateX = "0"; } }
  }
  render { Text("Sidebar"); }
}
```

`Z = Overlay` and `Transition = Slide` are ordinary token references, so layering and timing stay consistent
across every piece of chrome in the app — the same reason colors do.

## See also   {#see-also}
- [color palettes](https://osysharp.com/reference/ui/palette/) — turn one seed color into a full ramp with `Palette.From` and its named/numbered steps.
- [layout primitives](https://osysharp.com/reference/ui/layout/) — the layout primitives (`gap`/`align`/`justify`) you arrange components with.
- [component](https://osysharp.com/reference/ui/component/) — declaring a component and its `render` block.


---

<!-- https://osysharp.com/reference/ui/upload/ -->

# upload

> `Upload(onUploaded: Ingest) { … }` is a file picker wearing whatever you put inside it. When someone chooses a file its bytes are stored and your action receives an `UploadedFile` — the path they landed at, plus the name, type and size the browser reported. The bytes never pass through your code.

<!-- id: ui-upload · area: ui · stability: preview · html: https://osysharp.com/reference/ui/upload/ -->

## Summary        {#summary}
`Upload` is a **file picker you supply the appearance of**. Put anything inside it — a button, a row with an icon,
a whole drop zone — and it becomes the thing a person clicks to choose a file.

When they choose one, the bytes are stored and your action is called with an **`UploadedFile`**:

| field | what it is |
|---|---|
| `Path` | where the bytes were stored. This is the value you keep. |
| `FileName` | the original name the browser reported |
| `ContentType` | the type the browser reported |
| `Length` | the size in bytes, as the browser reported it |

The same value comes back from [camera and microphone](https://osysharp.com/reference/ui/capture/)'s `Camera.Capture()` and both `StopRecording()`s, so one
`Ingest(UploadedFile f)` serves a chosen file and a photograph alike.

## Signature      {#signature}
```osy syntax
Upload(onUploaded: Ingest) { Text("Choose a file"); }     // your own content is the button
Upload(accept: "image/*", onUploaded: Ingest) { … }       // narrow what the picker offers
```

## Description    {#description}

### The content inside it IS the button   {#content}
There is no built-in appearance to override. Whatever you render inside the `Upload` is what a person sees and
clicks — so it takes your icons, your spacing and your variants like anything else.

```osy title="a styled upload button" sample=file-manager/model/pages/manager.osy#UploadButton
```

### The bytes never reach your code   {#bytes}
By the time your action runs, the file is already stored. You receive a **path**, not a buffer — so a 9 MB
photograph costs your action nothing, and there is no way to accidentally hold one in a field.

What you do with the path is the interesting part: usually build a `FileAsset` from it, which is what gives the
file an owner, grants, deduplication and a quota. `demo/file-manager` is the worked version.

### What may be uploaded   {#types}
Images, audio, video, and a few text formats (`text/plain`, `text/markdown`, `text/csv`, `application/json`). The
limit is **10 MB** per file.

⛔ **SVG is deliberately refused**, even though it is an image. An SVG is a *document* that can carry script, so a
stored one served back from your own origin is a cross-site-scripting vector; every other image format is inert.
If you want vector art in your app, ship it as an asset instead — see [textures](https://osysharp.com/reference/ui/textures/) for which folder each kind of
art belongs in.

### Uploading is an authorized act   {#authorization}
The file store accepts writes from a **signed-in user**. An app with no users can show a picker and keep nothing —
and the refusal arrives as an error your action can catch and report, not as a silent nothing.

This is the same rule [camera and microphone](https://osysharp.com/reference/ui/capture/) runs into, and for the same reason: a file that arrives from a browser has an
owner, and an app with no users has nobody to be one.

### `accept:` narrows the picker, it does not enforce anything   {#accept}
`accept: "image/*"` tells the browser which files to offer by default. It is a convenience for the person choosing,
not a guarantee — the server decides what it will actually store, and refuses everything outside the list above.

## See also   {#see-also}
- [camera and microphone](https://osysharp.com/reference/ui/capture/) — the camera and microphone, which answer the same `UploadedFile`
- [Files (addressing something the app stores)](https://osysharp.com/reference/storage/index/) — what to do with the path once you have it
- [textures](https://osysharp.com/reference/ui/textures/) — art the app SHIPS, which is a different thing from a file a person uploads
- [component](https://osysharp.com/reference/ui/component/) — actions, and where an `onUploaded` handler lives


---

<!-- https://osysharp.com/reference/ui/web-fonts/ -->

# web fonts — shipping a typeface with your app

> Naming a font in your theme asks for it; `osy font add` ships it. The command pins a font file in your project's lock, every compile carries it to the server, and the app's stylesheet declares the matching `@font-face` — so the typeface renders on a machine that has never seen it, instead of falling through to the next name in your stack.

<!-- id: ui-web-fonts · area: ui · stability: stable · html: https://osysharp.com/reference/ui/web-fonts/ -->

## Summary        {#summary}
A theme names your app's type:

```osy syntax
theme Doc { Fonts { Body = "Hedvig Letters Serif, ui-serif, Georgia, serif"; } }
```

That is a **request**, not a delivery. `font-family` is a preference list, and a browser walks it until it finds
something it already has — so on any machine without *Hedvig Letters Serif* installed, the name is skipped silently
and your app renders in `ui-serif`. Nothing errors. The page just isn't the design.

**`osy font add` is the other half.** It registers a font FILE with your app: the file is pinned by content hash in
`osyrin.lock`, carried to the server by every compile, served back immutably, and declared as an `@font-face` in the
app's stylesheet. The theme still names the family exactly as before — the two stay separate on purpose, because one
family often needs several files (a regular, a bold, an italic) and the theme should name it once.

## Signature      {#signature}
```osy syntax
osy font add <file>  --family "<CSS family name>"  [--weight <400|700|…>]  [--style <normal|italic>]
osy font list
```

## Description    {#description}

### Registering a font    {#registering}

Put the file somewhere in your project and register it:

```console
$ osy font add model/fonts/HedvigLettersSerif-Regular.woff2 --family "Hedvig Letters Serif"
✓ Registered Hedvig Letters Serif (0ab846d39150…) → model/fonts/HedvigLettersSerif-Regular.woff2
  Name it in your theme to use it, e.g.
    Fonts { Body = "Hedvig Letters Serif, ui-serif, Georgia, serif"; }
  It ships on the next compile. You are responsible for its licence.
```

`--family` is **required and never guessed**. A filename is not a family name — `HedvigLettersSerif-Regular.woff2`
provides the family `Hedvig Letters Serif` — and a guess that is subtly wrong produces the exact failure this feature
exists to remove: the font loads, nothing matches it, and the page renders in the fallback while looking healthy.

Accepted formats are `.woff2`, `.woff`, `.ttf` and `.otf`. Prefer **woff2**: it is the smallest by a wide margin and
every current browser reads it.

`osy font list` shows what your app ships:

```console
$ osy font list
╭──────────────────────┬────────┬────────┬─────────────────────────────────────╮
│ Family               │ Weight │ Style  │ File                                │
├──────────────────────┼────────┼────────┼─────────────────────────────────────┤
│ Hedvig Letters Serif │ 400    │ normal │ model/fonts/HedvigLettersSerif-Reg… │
╰──────────────────────┴────────┴────────┴─────────────────────────────────────╯
```

### Naming it in your theme    {#naming}

Registering a file does not decide where it is used — your theme does, unchanged:

```osy title="a theme that names a shipped family" test app=ui-web-fonts
theme Doc {
  Fonts {
    Body    = "Hedvig Letters Serif, ui-serif, Georgia, serif";
    Heading = "Hedvig Letters Sans, ui-sans-serif, system-ui, sans-serif";
  }
}
```

Keep the fallbacks. They are what the reader sees during the moment before the file arrives, and on the rare browser
that cannot use it at all.

### More than one file per family    {#weights}

A family is a set of faces, and each file provides one. Register each with the weight and style it covers:

```console
$ osy font add fonts/Inter-Regular.woff2    --family "Inter"
$ osy font add fonts/Inter-Bold.woff2       --family "Inter" --weight 700
$ osy font add fonts/Inter-Italic.woff2     --family "Inter" --style italic
```

`--weight` defaults to `400` and `--style` to `normal`, so the first line above needs neither. Registrations are
keyed by **(family, weight, style)**, which is why the second and third lines ADD faces rather than replace the
first. A variable font that covers a range declares it as one: `--weight "100 900"`.

Your theme still names `Inter` once. The browser picks the right file per weight and style.

### What ships, and when    {#shipping}

Every compile carries every registered font whose file has changed or that the server does not yet have, and
re-verifies each file against its pinned hash first. **A font edited on disk but never re-added aborts the compile**
rather than shipping bytes the lock does not describe:

```console
$ osy compile
✗ font 'Inter': 'fonts/Inter-Regular.woff2' has changed since it was registered (osyrin.lock pins
  9f86d0818200…, file is 2c624232945…) — re-run `osy font add fonts/Inter-Regular.woff2 --family "Inter"` to re-pin it.
```

Files are stored by content address, so identical bytes are stored once no matter how many apps or versions
reference them, and a recompile that changes nothing ships nothing.

The app's stylesheet then declares each face ahead of the rules that use it:

```css
@font-face {
  font-family: "Hedvig Letters Serif";
  src: url("/_osy/font/0ab846d39150…") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
```

`font-display: swap` is deliberate: text is painted immediately in the fallback and re-painted when the file lands.
The alternative is a first paint with nothing where the words should be.

### Removing a font from the app   {#removing}

Delete its entry from `osyrin.lock` and recompile. The next compile reconciles the server to what your app now
declares — the row, the stored bytes and the `@font-face` all go together, so a stylesheet can never keep naming a
face you stopped shipping.

### Am I allowed to self-host this font? — licensing   {#licensing}

`osy font add` ships a file you chose, and the platform makes no licence check. Web-font licences vary in ways
tooling cannot infer — some permit self-hosting freely, some by domain, some not at all. Confirm you may
self-host before you register a font.

## Examples       {#examples}

The whole loop, from a downloaded file to a page that renders in it:

```console
$ osy font add model/fonts/HedvigLettersSerif-Regular.woff2 --family "Hedvig Letters Serif"
✓ Registered Hedvig Letters Serif (0ab846d39150…) → model/fonts/HedvigLettersSerif-Regular.woff2

$ osy compile
✓ Osyrin compiled and applied to MarkdownDemo (6 file(s)).
```

with the theme naming it:

```osy title="body text in a shipped serif" test app=ui-web-fonts-example
theme Doc {
  Fonts { Body = "Hedvig Letters Serif, ui-serif, Georgia, serif"; }
}
```

To confirm a font is really being used rather than silently falling back, **measure rendered text width** against a
family you know does not exist. `document.fonts.check()` is not a test — it resolves through the fallback stack and
answers `true` either way.

## See also       {#see-also}
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the `theme` block, and the `Fonts` tokens that name a family
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the other kind of file an app ships with its compile, on the same rail
- [app.osy](https://osysharp.com/reference/project/manifest/) — `osyrin.lock`, where a registration is pinned


---

<!-- https://osysharp.com/reference/workflow/inbox-act/ -->

# Acting on an inbox row (deposit, claim, release)

> Answer a queued slot from the row itself. The event is named at the call site because a queue's rows are heterogeneous — which slot a row is, and so which verb it takes, is known only when the queue is read.

<!-- id: workflow-inbox-act · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/inbox-act/ -->

## Summary        {#summary}
[`Workflow.Inbox<T>()`](https://osysharp.com/reference/workflow/inbox/) tells a person what is waiting for them. These three verbs are how they
answer it — from the row, without knowing in advance which slot it turned out to be.

## Signature      {#signature}
```osy syntax
Workflow.Deposit(row, <Event>(args…))   // answer the slot this row is
Workflow.Claim(row)                      // take an unassigned slot you are eligible for
Workflow.Release(row)                    // hand a claimed slot back to the pool
```

## Description    {#description}
### Why the event is named, and not called   {#naming}
Everywhere else a deposit is spelled `<Workflow>.For(entity).<Slot>.<Event>(…)` — every part of it written by the
author. A queue does not work that way. One row may be a manager's decision and the next a finance sign-off, so
`row.Approve(…)` cannot exist: the verb set differs per workflow, and a given row's verb is only known once the queue
has been read.

So the event is an **argument**, the same shape [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) uses when you raise one by name
(`OrderFlow.RaisePayment(order, 100)`) — and the split in checking follows from that:

| checked by | what it checks |
|---|---|
| the compiler | `Event` is declared by a workflow that `Tracks` `T`, and its arguments type-check |
| the run | this row's slot is actually waiting for that event — and `Candidates`, `Requires` and quorum |

Naming an event the row is not waiting for is refused, not deposited. That matters more than it sounds: arguments
bind to the arm **by name**, so a wrong event's payload would otherwise arrive as parameters nobody set.

### It is the same deposit   {#same-deposit}
There is one deposit in the platform and this is it. A row does not carry permission — obtaining it changes nothing
about who may act. A principal holding someone else's row and naming the right event is still refused by the slot's
own `Candidates`, because the queue has no rules of its own and deliberately nowhere to keep any.

### Claiming   {#claiming}
The queue shows unassigned work you are eligible for, so `Workflow.Claim(row)` takes it and `Workflow.Release(row)`
gives it back. Both need a principal — nothing can hold a slot on nobody's behalf. Only the current holder may
release.

To hand a slot to a **named colleague** rather than back to the pool, there is a third verb —
[`Workflow.Assign(row, principal)`](https://osysharp.com/reference/workflow/assign/). It is separated out because it is the one act here that asks
about the actor as well as the target: eligibility to hold work is not authority to move it.

## Examples       {#examples}
The morning screen, and the button on it:

```osy title="approve from the queue" test app=workflow-inbox-act
enum ClaimStage { Filed, Approved, Rejected }

[Principal] entity Employee {
  [Required] [MaxLength(80)] string DisplayName;
  security {
    allow read   when IsAuthenticated;
    allow create when IsAuthenticated;
  }
}

entity Invoice {
  [Required] [MaxLength(120)] string Title;
  [Required] decimal Amount;
  [Required] Employee Owner;
  ClaimStage Stage;
  security {
    allow read, update when IsAuthenticated;
    allow create       when IsAuthenticated;
  }
}

workflow ExpenseApproval {
  Tracks    = Invoice.Stage;
  Autostart = true;
  Initial   = Filed;

  event Decide(bool approved);

  state Filed {
    subscribe Decide(bool approved) as Manager { Assignee = this.Item.Owner; }
    on Manager(bool approved) {
      when (approved) { goto Approved; }
      default { goto Rejected; }
    }
  }

  terminal success Approved { }
  terminal error   Rejected { Message = "rejected"; }
}

void ApproveOldest() {
  var oldest = Workflow.Inbox<Invoice>()
                       .OrderBy(r => r.OpenedAt)
                       .First();
  Workflow.Deposit(oldest, Decide(approved: true));
}
```

Once it is answered the row leaves the queue, because the queue is re-read rather than remembered.

## Notes          {#notes}
**A fanned-out row acts on its own slot.** A row addresses the slot it is a row for, so one principal holding two
instances of the same fanned-out slot answers the one they picked — not whichever an alias lookup would have found.

**The compiler checks less here, and nothing enforces less.** The event and its arguments are supplied at the call
site rather than derived from a slot the author named, so a mistake that the static form would catch at compile time
is caught at run time instead. What is *allowed* is unchanged: the same authorization, the same requirements, the same
quorum.

## See also       {#see-also}
- [Workflow.Inbox&lt;T&gt; (what is waiting for me)](https://osysharp.com/reference/workflow/inbox/) — the queue these verbs act on
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — who may hold or satisfy a slot, and `<Wf>.For(item).<Slot>.Candidates(u)` for a screen
  deciding whether to OFFER the claim in the first place
- [Assign — handing a slot to a named colleague](https://osysharp.com/reference/workflow/assign/) — handing a slot to a named colleague, and the `Reassign` rule that permits it
- [Requires — named preconditions, and the live checklist](https://osysharp.com/reference/workflow/requires/) — the criteria a deposit must satisfy, and how to show them before the click
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — declaring the slot and the event it waits for


---

<!-- https://osysharp.com/reference/workflow/assign/ -->

# Assign — handing a slot to a named colleague

> Give a slot to somebody else. Claiming takes work for yourself and releasing puts it back in the pool; assigning is the third move — a hand-over. It answers two questions where a claim answers one: may the actor move this slot, and may the target hold it. The second is the slot's own `Candidates`; the first is `Reassign`, and without it the answer is the holder alone.

<!-- id: workflow-assign · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/assign/ -->

## Summary        {#summary}
**`Assign`** hands a slot to a named person. [`Claim`](https://osysharp.com/reference/workflow/inbox-act/) takes one **for the caller** and `Release`
puts it back in the pool; neither can express *"this is Bob's now"*.

The whole design is in one asymmetry. A claim asks **one** question — does the actor satisfy
[`Candidates`](https://osysharp.com/reference/workflow/candidates/) — because the actor and the new holder are the same person. An assign separates
them, so it asks **two**:

| question | asked of | answered by |
|---|---|---|
| may this slot be **moved** by you? | the **actor** | the holder rule, plus `Reassign` |
| may it be **held** by them? | the **target** | `Candidates`, exactly as a claim |

## Signature      {#signature}
```osy title="calling it: from a queue row, or from a page" syntax
Workflow.Assign(row, principal)                 // from a queue row
<Wf>.For(item).<Slot>.Assign(principal)          // from a page that already names the run
```

…and the declaration that says who besides the holder may do it:

```osy title="declaring who may MOVE the slot, not just hold it" syntax
subscribe Respond() as Reply {
  Candidates = u => u.Team == Team.Support;      // who may HOLD it
  Reassign   = a => a.IsSupervisor;              // who may MOVE it
}
```

## Description    {#description}

### Absent `Reassign` means the holder, and nobody else    {#fail-closed}
A slot that declares no `Reassign` can be handed on **by its current holder alone**. Not by a supervisor, not by
somebody who could have claimed it themselves — and, because an unheld slot has no holder, **not by anyone at all**
until the app says who may.

That is deliberate and it is the same posture as the rest of [the security model](https://osysharp.com/reference/security/secure-by-default/): a slot
is a commitment somebody made, so widening who can move it is a decision an app states rather than one it inherits.
Being eligible to hold work is not authority over work — which is why `Candidates` cannot stand in for this. A
supervisor who satisfies `Candidates` is refused on a slot that names nobody, and that case is worth internalising: it
is exactly where treating the two questions as one would let the wrong person through.

### The target meets the same gate a claimer would    {#target-eligibility}
`Candidates` is evaluated against the person receiving the slot. If they could not have claimed it, it cannot be
assigned to them. Otherwise an assign would be the way around the pool's own rule, and every `Candidates` in the app
would be advisory.

### An assign is not a claim    {#not-a-claim}
The row records which it was. `Assignee` moves to the new holder, and `ClaimedBy`/`ClaimedAt` are **cleared** — they
record who *took* the slot, and the new holder did not. Leaving the previous name there would read as a hold that
person no longer has.

The hand-over itself is on the [audit trail](https://osysharp.com/reference/workflow/audit/) as an `Assigned` event, whose actor is whoever moved it
and whose `PreviousAssignee` is who lost it.

### The clocks behave exactly as they do on a claim    {#clocks}
- the `Finished` promise **restarts** for the new owner — a budget somebody had no chance to meet is a hot potato, not
  a commitment;
- the [`Assigned { enter { } }`](https://osysharp.com/reference/workflow/milestone/) hook fires, so *"tell the new owner"* works for a hand-over and
  not only for a pickup;
- `AssignmentCount` goes up one, which is what makes churn countable without walking the trail.

`Expire` and `Deadline` never restart. They are the absolute caps, and they are what bounds passing work around.

**Assigning to the current holder does nothing** — no clock restart, no audit row. A fresh budget for a move that did
not happen would be the same hot potato by another route.

### Which of the two forms do I write?    {#forms}
Both do the same thing and meet the same gates.

- **`Workflow.Assign(row, principal)`** — from a [queue read](https://osysharp.com/reference/workflow/work/), where the row already names the run and
  the slot. This is the form a board or hand-over screen wants.
- **`<Wf>.For(item).<Slot>.Assign(principal)`** — from a page that knows the item and names the slot statically.

⚠ On a [dynamically fanned-out](https://osysharp.com/reference/workflow/fan-out-dynamic/) slot the static form addresses **the actor's own
instance** — *"hand mine on"* — because that is how an alias resolves for the acting principal. To move somebody
else's instance, use the row form, which names the one slot it is a row for.

### What is refused, and when    {#refusals}
| the compiler catches | the run catches |
|---|---|
| the target is not the app's `[Principal]` type | the actor may not move this slot |
| | the target does not satisfy `Candidates` |
| | the slot is already satisfied, cancelled or breached |
| | the slot has not opened yet (its [`After`](https://osysharp.com/reference/workflow/slot-dependencies/) predecessors are unsatisfied) |

Every refusal writes a `Refused` row to the trail before it throws, so a rejected hand-over is visible rather than
merely unsuccessful.

## Examples       {#examples}
A support desk where the reply slot may be moved by a supervisor and the park slot may not:

```osy title="a hand-over screen, and the rule that permits it" test app=workflow-assign
enum Stage { Working, Done }

[Principal] entity Agent {
  [Required, MaxLength(80)] string Name;
  bool IsSupervisor;
  bool OnDuty;
  security { allow read, create when IsAuthenticated; }
}

entity Ticket {
  [Required, MaxLength(120)] string Subject;
  Stage State;
  security { allow read, create, update when IsAuthenticated; }
}

workflow TicketFlow {
  Tracks    = Ticket.State;
  Autostart = true;
  Initial   = Working;

  event Fix();
  event Park();

  state Working {
    // A supervisor may move this one even though they do not hold it.
    subscribe Fix() as FixIt {
      Candidates = u => u.OnDuty;
      Reassign   = a => a.IsSupervisor;
    }
    // …and this one names nobody, so it is the holder's alone to hand on.
    subscribe Park() as Parked {
      Candidates = u => u.OnDuty;
    }
    on Complete { goto Done; }
  }

  terminal success Done { }
}

// The hand-over button on a queue screen: the row is already in hand, so the slot needs no naming.
void HandOver(Ticket ticket, Agent to) {
  var row = Workflow.Work<Ticket>()
                    .Where(r => r.SlotAlias == "FixIt" && r.Item == ticket)
                    .First();
  Workflow.Assign(row, to);
}

// The same move from a page that names the slot itself.
void HandOverParked(Ticket ticket, Agent to) {
  TicketFlow.For(ticket).Parked.Assign(to);
}
```

## Notes          {#notes}
**A `live` read over the run wakes on it.** An assign changes no property on the item, so it signals the item's
change channel deliberately — otherwise a page's [moves](https://osysharp.com/reference/workflow/transitions/), timeline and checklist would go on
showing the state from before the hand-over until something else happened to the row.

**An assign needs an acting principal.** Anonymous is nobody the app can have admitted, so there is nothing for the
authority gate to answer. Inside a test, that is `runas`.

**Inside a milestone body, `slot.Assign(p)` is a different thing.** A breach escalation or a follow-the-sun reminder
already runs as the app's own code with no acting principal, so no authority question arises there — the app is not
being asked whether it may move its own slot. The clock restart, the count and the audit row are the same.

**Who a screen may offer the button to** is [`<Wf>.For(item).<Slot>.Candidates(u)`](https://osysharp.com/reference/workflow/candidates/) — the same
predicate the engine evaluates, so a picker does not re-type the rule and drift from it.

## See also       {#see-also}
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — who may HOLD a slot, and how to ask before offering a button
- [Acting on an inbox row (deposit, claim, release)](https://osysharp.com/reference/workflow/inbox-act/) — `Claim` / `Release` / `Deposit`, the other acts on a queue row
- [Workflow.Work&lt;T&gt; (everything outstanding) and its SLA numbers](https://osysharp.com/reference/workflow/work/) — the board read the row comes from
- [Transitions — where this item may go next](https://osysharp.com/reference/workflow/transitions/) — where the item may go next, and which of those moves fill a wait
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — where the hand-over and every refusal are recorded


---

<!-- https://osysharp.com/reference/workflow/milestone/ -->

# Assigned / Finished (milestones)

> A milestone puts an SLA on a slot's progress — Assigned (someone must PICK IT UP within Within) and Finished (it must be SATISFIED within Within). Each carries a breach arm — Unassigned / Unfinished — that runs when the SLA lapses, and inside that arm the ambient `slot` is the slot the milestone hangs off: `slot.Assign(p)`, `slot.Candidates(u)`, `slot.Assignee`.

<!-- id: workflow-milestone · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/milestone/ -->

## Summary        {#summary}
A **milestone** puts a deadline on a slot's progress. There are two, by the two things that can be late:
`Assigned` — the slot must be **picked up** (claimed or pre-assigned) within `Within` — and `Finished` — the slot
must be **satisfied** (deposited into) within `Within`. Each has a **breach arm** that runs when its SLA lapses:
`Unassigned { … }` for Assigned, `Unfinished { … }` for Finished. A breach is **not** a failure by itself: an arm
with no `goto` runs its body and the wait lives on; an arm that `goto`s ends the wait.

## Signature      {#signature}
```osy syntax
Assigned {
  Within = <TimeSpan>;         // must be picked up within this
  Unassigned { <body> }        // ran if still Unassigned at Within (may `goto`)
}
Finished {
  Within = <TimeSpan>;         // must be satisfied within this
  Unfinished { <body> }        // ran if not Satisfied at Within (may `goto`)
}
```

## Description    {#description}
A milestone is declared where its SLA belongs — on a **slot** (inside its `subscribe … { }`), on a **state**, or on
the **workflow** as a cascade default; nearest declaration wins (slot over state over workflow). A cascade default is
inherited only by **pool slots** — a slot that declares `Candidates` — since the SLA models pool pickup (Assigned) and
completion (Finished) and its body reads `slot.Candidates`; a bare system-event slot (`subscribe Submit();`) is not
SLA-tracked. A slot may override just one clause (e.g. `Finished { Within = TimeSpan.FromDays(2); }`) and inherit the
rest. `Within` is a `TimeSpan` expression over `this.Item`, so different instances can carry different SLAs (snapshot
them in `Start { }`).

When the SLA lapses the milestone **breaches**: its breach arm runs, a `Breached` event is written to the timeline,
and — if the arm `goto`s — the run transitions. A milestone that has already been met never breaches (an Assigned
milestone is met the moment the slot leaves `Unassigned`; a Finished milestone the moment the slot is `Satisfied`), so
a breach that *fixes* the problem (e.g. `slot.Assign(lead)`) also stops it recurring.

**The ambient `slot`.** Inside a milestone body the bare identifier `slot` is the slot the milestone hangs off:

- **`slot.Assign(principal)`** — assign the slot to a principal (it becomes theirs; the wait continues).
- **`slot.Candidates(u)`** — evaluate the slot's own `Candidates` predicate against a principal `u` → a `bool`. It is
  the slot's eligibility rule, reusable in a query — `Person.Single(u => slot.Candidates(u) && u.IsLead)` finds the
  lead *among the slot's candidates* as one query.
- **`slot.Assignee` / `slot.Status` / `slot.IsUnassigned`** — read the slot's current holder / state.

**Naming it.** A breach arm may give the slot a name of its own — `Unassigned(Slot approver) { … }` — and then that
name is the slot throughout the body. It is the same one slot either way; naming it just reads better when the body
is about a person rather than a mechanism, and it matches how `on Expire(PoStatus state)` names what is ambient
there. The parameter is a `Slot` and there is exactly one of it.

```osy title="naming the slot so the body reads about a person" syntax
Assigned {
  Within = TimeSpan.FromHours(4);
  Unassigned(Slot approver) {                 // `approver` IS the slot — `slot` is simply its default name
    var lead = Person.Single(u => approver.Candidates(u) && u.IsLead);
    approver.Assign(lead);
  }
}
```

### `Retries` / `Backoff` / `Exhausted` — try again before giving up   {#retries}
For a slot a MACHINE fills — a child workflow, an external callback — the useful answer to a missed deadline is often
"try again", not "escalate":

```osy title="try again three times before escalating" syntax
Finished {
  Within  = TimeSpan.FromMinutes(5);
  Retries = 3;                          // three further windows
  Backoff = TimeSpan.FromMinutes(1);    // …each one a minute after the last failed
  Exhausted  { slot.Release(); }        // tried and gave up — hand it to the humans
  Unfinished { goto NeedsAttention; }   // …and this is where the run goes
}
```

Each retry re-opens the SAME window (`Within`), delayed by `Backoff`. When the attempts run out, `Exhausted { }` runs
and *then* the breach arm decides where the run goes.

- **A retry does not run the breach arm.** The breach arm may `goto`, so running it per attempt would move the run
  away on the first one and there would be no second attempt. The breach arm is the end of the story, not a step in it.
- **Every attempt is audited**, so the timeline shows three breaches and an exhaustion rather than one long silence.
- **The count is per owner.** A slot that changes hands gives its new holder a fresh set of attempts, for the same
  reason they get a fresh budget: attempts burned by someone else are not a commitment they made.
- **On a HUMAN slot you almost certainly want `Unassigned { … }` instead.** It lets you say *who* to escalate to and
  *when* to give up; `Retries = N` can only re-offer to the same pool on the same terms. It is allowed — three
  re-offers is odd, not wrong — but it is rarely what you meant.

`Backoff` takes either a plain time span — the same wait every time — or a **[retry policy](https://osysharp.com/reference/workflow/backoff/)**, which
says how the wait GROWS and bounds it:

```osy title="a wait that grows, instead of the same wait each time" syntax
Finished {
  Within  = TimeSpan.FromMinutes(5);
  // Three attempts, five minutes apart, then ten — but never more than an hour idle.
  Backoff = Backoff.Exponential(TimeSpan.FromMinutes(5)).MaxAttempts(3).Cap(TimeSpan.FromHours(1));
  Exhausted  { slot.Release(); }
  Unfinished { goto NeedsAttention; }
}
```

⚠ **`Retries = N` and a policy's `.MaxAttempts(M)` are two budgets for one thing, and a milestone declaring both is a
compile error.** They also count differently — `Retries` is how many windows follow the first, `MaxAttempts` is how
many there are in total — so `Retries = 2` and `.MaxAttempts(3)` say the same thing. Pick whichever reads better where
you are; see [Backoff (retry policy)](https://osysharp.com/reference/workflow/backoff/).

### `enter { }` — the success counterpart of the breach arm   {#enter}
A milestone has two outcomes and both have a home. The breach arm runs when the SLA is missed; **`enter { }` runs when
the milestone is REACHED** — the slot got an owner (`Assigned`), or it was satisfied (`Finished`).

```osy title="telling the new owner the moment the slot is taken" syntax
Assigned {
  Within = TimeSpan.FromHours(4);
  enter  { Notify(slot.Assignee); }        // it has an owner now — tell them
  Unassigned { … }                          // …and this is what happens if it never did
}
```

This is the only home for "tell the new owner", and that is why it exists: **claiming a slot changes no workflow
state**, so no state `enter { }` fires. Without it, an approval app has nowhere to put the most ordinary thing it
does.

- **It runs on EVERY assignment, not just the first.** A slot released and re-claimed enters `Assigned` again, and the
  new owner is told — the same reasoning that gives each holder their own `Finished` budget. A hook that fired once
  would go quiet exactly when a slot changes hands, which is when someone most needs to hear.
- **The ambient `slot` is bound**, so `slot.Assignee` is the person who just took it.
- **It may not `goto`.** Reaching a milestone is orthogonal to workflow state: assignment is about WHO, not about
  WHERE the run is. Side effects only — the compiler refuses a `goto` here.
- It is available on `Finished` too (fires when the slot is satisfied), though that is often already covered by the
  `on <Event>` arm that handled the deposit.

## Examples       {#examples}
```osy title="a deadline on getting the slot OWNED" test app=workflow-milestone
enum Decision   { Approve, Reject }
enum OrderState { Review, Done }

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

entity Order {
  [Required, MaxLength(60)] string Reference;
  [Required] Person Requester;
  OrderState Status;                       // no default: the workflow owns this field
  security { allow read, create, update when IsAuthenticated; }
}

workflow ReviewFlow {
  Tracks    = Order.Status;
  Autostart = true;
  Initial   = Review;

  event Approve(Decision decision);

  state Review {
    subscribe Approve(Decision decision) as Legal {
      Candidates = u => u.Email != "";
      Assigned {
        Within = TimeSpan.FromHours(4);
        Unassigned { }
      }
    }
    on Approve(Decision decision) { goto Done; }
  }
  terminal success Done { }
}
```

Breach ≠ failure — nobody picked up the pool slot, so on breach we widen it to the department lead and keep waiting:

```osy title="Assigned breach assigns the lead" syntax
subscribe Approve(Decision decision, string reason) as Legal {
  Candidates = u => u.Department == Dept.Legal;
  Assigned {
    Within = TimeSpan.FromHours(4);
    Unassigned {                                   // no goto → keep waiting, now with an owner
      var lead = Person.Single(u => slot.Candidates(u) && u.IsLead);
      slot.Assign(lead);
    }
  }
}
```

A claimed slot that goes overdue *is* fatal — the `goto` ends the wait:

```osy title="Finished breach escalates" syntax
Finished {
  Within = TimeSpan.FromHours(8);
  Unfinished { goto Escalated; }
}
```

## See also       {#see-also}
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the slot a milestone hangs off (its `Candidates` / `Assignee`)
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — the same `Candidates(u)` question OUTSIDE a milestone, where the run is named rather
  than ambient: `<Wf>.For(item).<Slot>.Candidates(u)`
- [Remind (milestone reminders)](https://osysharp.com/reference/workflow/remind/) — nudges scheduled off a milestone before it breaches
- <span class="planned" title="this page is planned and not written yet">workflow-route</span> — the state-level `on … goto …` routes a breach `goto` joins
- <span class="planned" title="this page is planned and not written yet">workflow-state</span> — the enclosing state and its `Expire` deadline


---

<!-- https://osysharp.com/reference/workflow/automatic-durability/ -->

# Automatic durability (steps you do not have to write)

> Any call that leaves the platform — an outbound client call, an external service — is made a durable step by the compiler. In a workflow, a crash-resume reuses what the call already returned instead of making it a second time. You write an ordinary call; you do not mark it, and you cannot forget to.

<!-- id: workflow-automatic-durability · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/automatic-durability/ -->

## Summary        {#summary}
A workflow body can run more than once — that is how the platform survives a crash: work that did not commit is simply
done again. Re-running ordinary computation is harmless. Re-running a **payment**, an **email**, or any call into a
system that is not yours is a second charge and a second message.

[Workflow.Once (run a step at most once)](https://osysharp.com/reference/workflow/once/) is the explicit way to say "not twice". **You rarely need it**, because the compiler already knows
which calls leave the platform, and wraps each one in a durable step for you. The call you write is the call you read;
the durability is not something you remember to add.

## Description    {#description}

### What gets a step   {#what-steps}
Exactly one thing: **a call that leaves the platform.** An outbound client operation is an HTTP request to somebody
else's system, so a second execution is a second request. That is the whole rule, and it is decided per *call*, not per
function.

```osy title="one outbound call becomes a step, with nothing marked" syntax
void Notify(Order order) {
  Mailer.Send(new SendRequest { To = order.Email });   // a durable step, automatically
}
```

Nothing marks it. Nothing has to.

### Each call is its own step   {#per-call}
If a function makes two outbound calls, they are **two steps**, not one:

```osy title="two outbound calls are two steps, not one" syntax
void NotifyBoth(Order order) {
  Mailer.Send(new SendRequest { To = order.Email });     // step 1
  Shipping.Book(new BookRequest { Id = order.Code });    // step 2
}
```

This is the part that matters. If the whole function were one step, a crash *between* the two calls would re-run the
email on resume. Because each call is its own step, a resume finds the email already recorded, skips it, and picks up
at the booking. The ordinary code between the two steps re-runs freely — it is just a computation over results that are
already recorded.

### What does not get a step   {#not-steps}
Values that are merely **unrepeatable** — the current time, a new identifier, a random number — are not steps. Nothing
about them leaves the platform, and a resumed run already sees the same value it saw the first time. They cost nothing
to protect and are protected anyway.

Ordinary reads, writes and computation are not steps either. Re-running them is correct: the work that did not commit
is redone, which is the point.

### Outside a workflow   {#outside}
The same function is often called from a workflow *and* from an ordinary request. There, there is no run to record
against — so the call simply happens, exactly as the source reads. You do not write the function twice, and you do not
choose in advance which kind of caller it is for.

### When you still write it yourself   {#vs-once}
Reach for [Workflow.Once (run a step at most once)](https://osysharp.com/reference/workflow/once/) when you want something automatic durability deliberately does not do:

- **An idempotency key.** A recorded result means the step is not *re-run*; it cannot undo a call that already reached
  the other system and was lost on the way back. Only that system can recognise a retry, and only from a key you pass
  it. `Workflow.Once("step", key => …)` hands you a stable one.
- **Skipping expensive but harmless work.** Re-running a long pure computation is correct, just wasteful. `Once` is
  also how you say "do not redo this".
- **Your own boundary.** Grouping several calls into one step, or pinning one specific result.

Writing `Once` around a call that would have been lowered anyway gives you **one** step, not two — yours, with whatever
you asked for.

## Examples       {#examples}
```osy title="the classification is the compiler's, and it will tell you" test app=workflow-automatic-durability
enum OrderState { Placed, Fulfilled }

entity Order {
  [Required, MaxLength(60)] string Reference;
  decimal Total;
  OrderState Status = OrderState.Placed;
  security { allow read, create, update when IsAuthenticated; }
}

class ChargeResult { string? Receipt; }

// An ordinary typed client. Nothing about it says "durable".
client Payments {
  BaseUrl = "https://api.payments.example";
  [Post("/charges")]
  ChargeResult Charge([Query] string reference, [Query] decimal amount);
}

// Nothing here is marked either. The call that LEAVES the platform is what makes this a durable
// step — and `osy model --json` reports both the verdict and the route to it, so you never guess:
//   "durability": "External", "durabilityVia": "Payments.Charge"
void Fulfil(Order order) {
  Payments.Charge(order.Reference, order.Total);   // a step: never re-charged on resume
  order.Status = OrderState.Fulfilled;             // ordinary work — re-runs freely
}
```


An agent step and a charge, with no ceremony at all:

```osy title="a decision and a charge, with no ceremony at all" syntax
void Fulfil(Order order) {
  var decision = Assistant.Decide(order.Summary);   // a step: never re-charged, never re-decided on resume
  Payments.Charge(new ChargeRequest { Amount = order.Total });   // a separate step
  order.Status = Status.Fulfilled;                  // ordinary work — re-runs freely if the run resumes
}
```

The same call made exactly-once at the other end, by asking for a key:

```osy title="taking an idempotency key for the far side" syntax
void Charge(Order order) {
  Workflow.Once("charge", key => Payments.Charge(new ChargeRequest { Amount = order.Total, idempotencyKey: key }));
}
```

## Notes          {#notes}
- **The guarantee is at-least-once execution, at-most-once *result*.** A crash in the instant after a call returns and
  before its result is recorded will run it again. No engine can close that window from this side — the call already
  happened in someone else's system. An idempotency key is the only thing that can, which is why one is offered.
- **A recorded step can outlive work that was rolled back.** If the surrounding transaction rolls back after the call
  escaped, the record still says it ran — because it did.
- **Records belong to the run.** Two runs doing the same work each do it once.
- **It composes with app versions.** A recorded result is data belonging to the run, so a redeploy does not disturb it
  — see [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/).

## See also       {#see-also}
- [Workflow.Once (run a step at most once)](https://osysharp.com/reference/workflow/once/) — writing a step yourself, and the idempotency key.
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — start a workflow, and optionally wait for it.
- [Workflow.BeginSaga (a compensating saga scope)](https://osysharp.com/reference/workflow/beginsaga/) — compensating steps, for work that must be *undone* rather than not repeated.
- [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/) — what a run in flight keeps executing across a deploy.


---

<!-- https://osysharp.com/reference/workflow/backoff/ -->

# Backoff (retry policy)

> A retry policy as a value: how long to wait before the next attempt. Three shapes say how the wait GROWS — `Fixed` (the same wait every time), `Linear` (it grows by the interval), `Exponential` (it doubles) — and three fluent bounds keep it safe: `.MaxAttempts(n)` how many attempts there are, `.Cap(max)` how long any one wait may get, `.Jitter(f)` how much to spread them so many runs do not retry in lockstep. Anywhere a policy is taken, a plain `TimeSpan` is still legal and means `Fixed`.

<!-- id: workflow-backoff · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/backoff/ -->

## Summary        {#summary}
A **`Backoff` is a retry policy you can hold** — a value that answers one question, *how long to wait before attempt
N*. It deliberately does **not** answer *whether* to retry: that belongs to whatever is doing the retrying (a
milestone's attempt budget, a step's failure), and a policy that answered both would be two things under one name.

Three factories say how the wait **grows**, and three fluent members **bound** it. The bounds are members rather than
more arguments because they are all numbers: nobody reading `Backoff.Exponential(2s, 4, 5m)` can say which is which,
and `.MaxAttempts(4).Cap(TimeSpan.FromMinutes(5))` says it.

## Signature      {#signature}
```osy syntax
Backoff.Fixed(<TimeSpan>)          // 2s, 2s, 2s, 2s …
Backoff.Linear(<TimeSpan>)         // 2s, 4s, 6s, 8s …
Backoff.Exponential(<TimeSpan>)    // 2s, 4s, 8s, 16s …

  .MaxAttempts(<int>)              // how many attempts in TOTAL (the first one included)
  .Cap(<TimeSpan>)                 // no single wait may exceed this
  .Jitter(<decimal>)               // spread each wait uniformly ± this fraction of itself
```

## Description    {#description}

### The three shapes   {#shapes}
The name says the sequence. With an interval of 2 seconds:

| policy | the waits |
|---|---|
| `Backoff.Fixed(TimeSpan.FromSeconds(2))` | 2s, 2s, 2s, 2s |
| `Backoff.Linear(TimeSpan.FromSeconds(2))` | 2s, 4s, 6s, 8s |
| `Backoff.Exponential(TimeSpan.FromSeconds(2))` | 2s, 4s, 8s, 16s |

The first wait is always the interval — attempt 1 is the first *retry*, and the original try was not a retry.

### `.Cap(…)` — the bound that makes exponential safe to write down   {#cap}
Doubling is the growth people mean and the ceiling is the part they forget. `Backoff.Exponential(2s)` on its tenth
attempt waits **17 minutes**; on its fifteenth, **9 hours**. Without a cap the interesting parameter becomes the
attempt count, which is the wrong knob — you wanted "keep trying, but never sit idle longer than five minutes":

```osy syntax
Backoff.Exponential(TimeSpan.FromSeconds(2)).Cap(TimeSpan.FromMinutes(5))
// 2s, 4s, 8s, 16s, 32s, 64s, 2m8s, 4m16s, 5m, 5m, 5m …
```

### `.Jitter(…)` — so a herd does not retry in lockstep   {#jitter}
When one dependency goes down, every run waiting on it computes the *same* delay and comes back at the *same*
instant — which is the outage's second wave. `.Jitter(0.2)` spreads each wait uniformly ±20% of itself.

Jitter is **opt-in, and the default is exact**. That is what makes a retry sequence assertable in a test, and what
makes a run's timeline read as a sequence rather than a scatter. Reach for it when many runs retry against one
shared dependency; leave it off otherwise.

### `.MaxAttempts(…)` — how many, in TOTAL   {#max-attempts}
`.MaxAttempts(3)` means **three attempts**, not three retries after the first.

⚠ **A durable step's `retry:` REQUIRES it.** A milestone can leave it out because `Retries = N` supplies the budget;
a step has nothing else in scope, so an uncapped policy there would retry for ever and is a compile error. See
[Workflow.Once (run a step at most once)](https://osysharp.com/reference/workflow/once/).

⚠ **A milestone's `Retries = N` counts the other way** — it is how many *further* windows follow the first, which is
W43's original wording and is not being changed. So `Retries = 2` and `.MaxAttempts(3)` describe the same thing. A
milestone that declares **both** is a compile error rather than a silent preference, because the two do not even
count the same unit:

```console
this milestone sets `Retries = 2` and its `Backoff` policy also caps the attempts with `.MaxAttempts(…)` —
they are two budgets for one thing. Keep ONE: drop `.MaxAttempts(…)` and leave `Retries = 2`, or drop
`Retries` and write `.MaxAttempts(3)` on the policy. ⚠ They count differently — `Retries` is how many
FURTHER windows follow the first, `MaxAttempts` is how many windows there are in TOTAL.
```

### A plain `TimeSpan` is still a policy   {#timespan}
Everywhere a `Backoff` is accepted, a bare `TimeSpan` is too, and it means `Fixed` — the same wait every time.
Nothing already written changes meaning, and `Backoff = TimeSpan.FromMinutes(30);` stays the shortest way to say the
simplest thing.

## Examples       {#examples}
On a milestone, the policy is what delays each further window. Here a machine-filled slot gets three attempts whose
gaps double, so a dependency that is briefly unavailable is retried quickly and a genuinely broken one is not
hammered:

```osy title="a milestone whose retry waits double" test app=workflow-backoff-milestone
enum JobStage { Queued, Running, Escalated }
enum Decision { Ok }

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

entity Job {
  [Required, MaxLength(120)] string Title;
  JobStage Stage;
  security { allow read, create, update when IsAuthenticated; }
}

workflow JobFlow {
  Tracks    = Job.Stage;
  Autostart = true;
  Initial   = Queued;

  event Start();
  event Complete(Decision decision);

  state Queued { subscribe Start(); on Start { goto Running; } }

  state Running {
    subscribe Complete(Decision decision) as Worker {
      Finished {
        Within  = TimeSpan.FromMinutes(5);
        // Three attempts, five minutes apart, then ten, then twenty — but never more than an hour idle.
        Backoff = Backoff.Exponential(TimeSpan.FromMinutes(5)).MaxAttempts(3).Cap(TimeSpan.FromHours(1));
        Exhausted  { }                        // tried and gave up
        Unfinished { goto Escalated; }        // …and this is where the run goes
      }
    }
    on Worker(Decision decision) { default { goto Escalated; } }
  }

  terminal error Escalated { Message = "job escalated"; }
}
```

The same policy written the other way round — an attempt budget on the milestone, growth on the policy:

```osy title="the same policy with the budget on the milestone" syntax
Finished {
  Within  = TimeSpan.FromMinutes(5);
  Retries = 2;                                          // two further windows after the first
  Backoff = Backoff.Exponential(TimeSpan.FromMinutes(5));
  Unfinished { goto Escalated; }
}
```

Many runs retrying against one shared dependency, spread so they do not arrive together:

```osy title="spreading many runs so retries do not arrive together" syntax
Backoff = Backoff.Exponential(TimeSpan.FromSeconds(2))
                 .Cap(TimeSpan.FromMinutes(5))
                 .Jitter(0.2)
                 .MaxAttempts(8);
```

## See also       {#see-also}
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — `Retries` / `Backoff` / `Exhausted`, the milestone that consumes a policy
- [Workflow.Once (run a step at most once)](https://osysharp.com/reference/workflow/once/) — `retry:`, the other consumer: a durable step that FAILED rather than a deadline that passed
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the slot a milestone hangs off


---

<!-- https://osysharp.com/reference/workflow/callback-url/ -->

# Callback URLs — letting an outsider complete one slot

> Mint a single-use link that completes exactly one waiting slot, for a third party who has no account and cannot sign in. The link is the permission: it carries an unguessable token, works once, dies with the slot it was made for, and can never touch anything else.

<!-- id: workflow-callback-url · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/callback-url/ -->

## Summary        {#summary}
Some of the people a workflow waits on will never have a login. A supplier confirming a delivery date, a customer
approving a quote, a lab returning a result — asking them to create an account to click "yes" is asking them not to
answer.

`CallbackUrl()` mints a link for exactly that. Call it on a slot alias inside the workflow, email the result, and
whoever holds it can complete **that one slot** — no sign-in, no account, nothing else reachable.

```osy syntax
enter {
  var url = SupplierOk.CallbackUrl();
  Email.Send(this.Item.Supplier.Email, $"Confirm this order: {url}");
}
```

The link is the permission. That is a real decision rather than a shortcut, so the whole of it is written down under
[[#description|What makes the link safe]] — read that before you send one somewhere it could be forwarded.

## Signature      {#signature}
```osy syntax
<SlotAlias>.CallbackUrl()   // returns string — an absolute URL for this slot on this run
```

Callable inside a workflow handler body — a state's `enter`, a route arm, a milestone — where the alias of a
[`subscribe`](https://osysharp.com/reference/workflow/subscribe/) in that state is in scope. It returns a plain `string`, so it goes into an email,
onto the tracked row, or wherever the app needs it.

## Description    {#description}

### The other side: what the holder does with it   {#posting}
The link is answered by **POSTing the event's own arguments** to it as JSON:

```text
POST https://orders.example.com/api/workflow/callback/<token>
Content-Type: application/json

{ "ok": true }
```

The body is the slot's event signature, by name — `subscribe Confirm(bool ok)` takes `{"ok": true}` and nothing else.
It is checked, not trusted: a missing parameter, a wrong type, or a property the event never declared is refused with
a message saying what the response takes. An event with no parameters is answered by posting nothing at all.

It is a POST rather than a GET on purpose. A link that acted on being *fetched* would be spent by the first mail
scanner or link preview that touched it, before the human ever clicked.

### What makes the link safe   {#safety}
A callback link deliberately bypasses [`Candidates`](https://osysharp.com/reference/workflow/candidates/) — there is no principal for a pool to
admit, which is the entire point. Five properties bound it, and together they are why that is sound:

| | |
|---|---|
| **Unguessable** | 256 bits of cryptographic randomness. Guessing one is not a slow attack; it is not an attack. |
| **Single-use** | The deposit that succeeds burns it. A *forwarded* link cannot be answered by a second party — which is a different problem from a double click, and the one that actually bites. |
| **One slot, one run** | It completes the slot it was minted for and nothing else. A leaked link risks exactly one wrong answer on one item; it can never be replayed against another. |
| **Slot-lifetime** | It works only while the slot is open. Once the item is completed, cancelled or its deadline has passed, the link is dead — a link that still works after the ticket closed is a bug. |
| **Never stored** | Only a hash of the token is kept. Reading the database — a backup, a support query — does not hand anyone the ability to answer. |

The link is returned **once**, from the call. Nothing can read it back afterwards, so put it where you need it in the
same body that minted it.

### What it does not relax   {#still-enforced}
A callback is a way *in*, not a way *around*. Everything else the slot declares still applies:

- a slot's [`Requires`](https://osysharp.com/reference/workflow/subscribe/) must still hold — "you may not resolve without a root cause" is as true
  for a supplier as for a colleague;
- a slot still waiting on its `After` predecessors is not open, and the link is refused until it is;
- a run that has already finished accepts nothing.

### Is `[Authorize]` evaluated on a callback deposit?    {#authorize}
⚠ **An event's [[workflow-authorize|`[Authorize]`]] predicate is not evaluated either**, and for the same reason
`Candidates` is not: a predicate takes a principal and a callback deposit has none. There is nothing to evaluate it
against, so it is skipped rather than failed.

This is worth stating plainly because the two declarations look like they compose and do not:

```osy syntax
[Authorize(u => u.Email == this.Item.Email)]   // nobody may accept on your behalf — TRUE of the button
event Accept();
…
enter { this.Item.AcceptLink = Acceptance.CallbackUrl(); }   // …and the LINK is not governed by it
```

Both are correct and both are wanted — that IS an invite flow — but the second widens the first, and only the author
can decide that is what they meant. **Whoever can read the mail can complete the slot**, which is exactly the
authority a real emailed link carries. If that is not acceptable for a given event, do not mint a URL for it; there
is no way to have the link and the predicate at once.

Pin it in a test with [`Workflow.Redeem`](https://osysharp.com/reference/testing/redeem-callback/), asserting the refusal and the bypass together —
a gate that stopped evaluating `[Authorize]` for everybody would pass either half alone.

### Minting it again   {#reminting}
Calling `CallbackUrl()` a second time for the same slot issues a **new** link and retires the old one. That is what a
resend should do — sending to a corrected address must not leave the first address still able to answer.

### One slot at a time   {#no-fan-out}
It cannot be called on a [fanned-out](https://osysharp.com/reference/workflow/fan-out/) slot: one alias there is one slot *per element*, so a single
link could not say which one it completes. The compiler refuses it rather than picking.

## Examples       {#examples}

A supplier who is not a user of the app confirms an order. Note that `Candidates` admits only staff — the supplier is
not in the pool, and the link is the only way they can answer:

```osy title="supplier confirmation" test app=workflow-callback-url
enum OrderState { Awaiting, Confirmed, Refused }

[Principal]
entity Person {
  [Required, MaxLength(100)] string Name;
  bool IsStaff;
  security { allow read when IsAuthenticated; allow create when IsAuthenticated || IsAnonymous; }
}

entity Order {
  [Required, MaxLength(100)] string Reference;
  [Required, MaxLength(200)] string SupplierEmail;
  OrderState State = OrderState.Awaiting;
  [MaxLength(400)] string? ConfirmLink;
  security { allow read when IsAuthenticated; allow create, update when IsAuthenticated || IsAnonymous; }
}

workflow OrderFlow {
  Tracks  = Order.State;
  Initial = Awaiting;

  event Confirm(bool ok);

  state Awaiting {
    subscribe Confirm(bool ok) as SupplierOk {
      Candidates = u => u.IsStaff;
    }

    enter {
      this.Item.ConfirmLink = SupplierOk.CallbackUrl();
    }

    on SupplierOk(bool ok) {
      when (ok) { goto Confirmed; }
      default   { goto Refused; }
    }
  }

  terminal success Confirmed { }
  terminal cancel  Refused { }
}
```

The supplier answers it with a single request:

```text
POST https://orders.example.com/api/workflow/callback/8Kj2mQ...   {"ok": true}
```

### The link survives a deploy — including one that changes the event   {#deploys}
You cannot recall a URL that is already in somebody's inbox, and the person holding it has no way to learn you
redeployed. So the guarantee is not "the link works until the next deploy": **a link keeps working across deploys, and
across a change to the very event it completes.**

The token records the version it was minted under. When the body arrives it is read against THAT version's signature
and translated forward into the one the run is on now, using the mappings each deploy authored. A deploy that changes
an event's parameters without saying what the old shape means is refused — see `map event` in [Migrating runs that are still in flight](https://osysharp.com/reference/workflow/migration/).

What DOES end a link is what always ended it: the slot closing, the deposit burning it, or an author's
`drop slot`, which cancels the claim deliberately.

## See also       {#see-also}
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the slot the link completes, and its `Requires` gate
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — the pool a callback deliberately bypasses, and why that is bounded
- [Raising a workflow event](https://osysharp.com/reference/workflow/raise/) — advancing a run from app code, where a principal does exist
- [Acting on an inbox row (deposit, claim, release)](https://osysharp.com/reference/workflow/inbox-act/) — a signed-in person answering their own queued item instead


---

<!-- https://osysharp.com/reference/workflow/candidates/ -->

# Candidates (slot)

> Declares WHO may hold or satisfy a `subscribe` slot. `Candidates` is one expression surface that dispatches on its return type: a `principal => bool` PREDICATE selects the eligible pool by a rule; a `() => List<Principal>` COMPUTATION returns the eligible set outright, computed off the run's data (`this.Item` is in scope). A principal not eligible is refused when they try to claim or deposit. Returning anything else is a compile error.

<!-- id: workflow-candidates · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/candidates/ -->

## Summary        {#summary}
`Candidates` on a `subscribe` slot declares WHO may **hold or satisfy** that slot. It is a single expression surface
that the compiler **dispatches on by return type**:

- returns **`bool`** → a **predicate** (`u => u.Team == Team.Support`): the eligible pool is every principal for whom the
  rule holds.
- returns **`List<Principal>`** → a **computation** (`() => Agent.Where(u => u.Region == this.Item.Region &&
  u.OnCall).ToList()`): the eligible set is exactly the list you return, computed fresh off the run's data.

Either way a principal who is **not eligible** is refused when they try to claim or deposit. Returning any other type
(a scalar, a single principal) is a **compile error**. This is who-may-**HOLD** authorization — a different question from
`[Authorize]`, which is who-may-**RAISE** an event.

## Signature      {#signature}
```osy syntax
subscribe <Event>() as <Alias> {
  Candidates = <principal> => <predicate>;              // a bool predicate  → the pool by a rule
  // — or —
  Candidates = () => <expression returning List<Principal>>;   // a computation → the eligible set
}
```

## Description    {#description}
A slot with `Candidates` is a **pool slot**: work that any eligible principal may pick up. The gate is a **membership
check** in both forms — "is this principal one of the eligible set?" — evaluated fail-closed every time a principal
tries to claim the slot or deposit into it.

### The predicate form — a rule   {#predicate}
The predicate form is a single-parameter lambda whose parameter is the principal being tested, typed as the app's
`[Principal]` entity. `this.Item` (the tracked entity) is in scope, so the rule can compare the principal to the item —
a role test, an ownership test, a four-eyes exclusion:

```osy title="a bool predicate — the pool by a rule" syntax app=support-triage
subscribe Decide() as Legal {
  Candidates = u => u.Department == Dept.Legal && u != this.Item.Requester;   // four-eyes
}
```

### Reading a sibling slot — cross-slot four-eyes   {#sibling-slot}
A rule often has to exclude **whoever already acted**, not whoever raised the item. Name the sibling slot by its `as`
alias and read who holds it:

```osy title="the second approver may not be the first" test app=workflow-candidates-four-eyes
enum PoStage { Review, Done }

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

entity Po {
  [Required, MaxLength(120)] string Title;
  PoStage Stage;
  security { allow read, create, update when IsAuthenticated; }
}

workflow PoApproval {
  Tracks    = Po.Stage;
  Autostart = true;
  Initial   = Review;

  event Approve();

  state Review {
    subscribe Approve() as First { }

    subscribe Approve() as Second {
      After      = [First];                    // First is satisfied before this opens…
      Candidates = u => u != First.Assignee;   // …so its assignee is who really approved
    }

    on Complete { goto Done; }
  }

  terminal success Done { }
}
```

Three members read off a sibling: **`.Assignee`** (the principal holding it, or nothing while it is unheld),
**`.Status`**, and **`.IsUnassigned`**. They answer for **this run**, so each item in flight is judged against its own
history rather than against a rule written once for all of them.

⚠ **Order is part of the rule, so make it explicit.** The gate is evaluated when someone tries to claim or deposit, and
a slot nobody holds yet has no assignee to exclude — so `u != First.Assignee` admits everyone until First is taken.
[`After`](https://osysharp.com/reference/workflow/slot-dependencies/) is what makes the exclusion mean what it reads like: with it, the second slot
does not exist to anybody until the first is satisfied.

⚠ **A FANNED-OUT sibling is refused**, because that alias names one slot per element and the read cannot say which.
Answering with an arbitrary instance would admit exactly the people the other instances exclude — a wrong *allow*,
which is the one direction an authorization rule must never fail in. Compare against a slot that names exactly one, or
decide it in the route arm once the fan-out is satisfied.

### The computation form — the set   {#computation}
The computation form returns the eligible set as a `List<Principal>` — arbitrary Osy# that queries or assembles the
list. `this.Item` is **ambiently in scope**, so the set is computed against the run's own data. The primary shape is a
zero-parameter lambda; a named function that returns a list works too. It is computed **fresh, on demand** each time
the gate runs — never stored or materialised:

```osy title="a List<Principal> computation — the eligible set, computed off the run" syntax app=support-triage
subscribe Handle() as Owner {
  Candidates = () => Agent.Where(u => u.Region == this.Item.Region && u.OnCall).ToList();
}
```

Use the computation form when eligibility is a **query over data** rather than a rule over one principal — "the on-call
agents in this ticket's region", "everyone on the account team for this order" — especially when the set depends on
relationships the item points at.

### Return-type dispatch and errors   {#dispatch}
The compiler decides the form from the resolved return type: `bool` → predicate, `List<Principal>` → computation. A
`Candidates` that returns anything else is rejected at compile time:

```text
`Candidates` must return either `bool` (a `principal => predicate`) or `List<Principal>`
(a computation returning the eligible set) — got 'Agent'.
```

### Fail-closed   {#fail-closed}
In both forms the gate refuses when it cannot positively establish membership: no acting principal, no `[Principal]`
entity, an unresolvable principal, or an empty computed set → **not a candidate**. A refused claim or deposit throws and
is recorded on the workflow's audit timeline; the entity does not move.

### Acting on a pool slot does NOT claim it   {#acting-does-not-claim}
A pool slot — one with `Candidates` and no `Assignee` — is held by **nobody** until someone calls `Claim()`. Depositing
its event satisfies the slot **without ever assigning it**, so `Assignee` is still nothing inside the route arm. That
is correct and deliberate: `Assignee` records *whose queue this sat in*, and on a pool slot nobody ever queued it.

**To credit the decision, name the actor — not the assignee.** [`actor`](https://osysharp.com/reference/workflow/actor/) is the principal whose
action drove the body, and it has an answer whether the slot was claimed, unclaimed, or acted on by somebody it was
never assigned to:

```osy title="credit who decided, not whose queue it was in" syntax
on Legal(Decision decision, string reason) {
  this.Item.ReviewedBy = actor;   // WHO DECIDED — always answers
}
```

⚠ **`slot.Assignee` is the trap here**, and it fails quietly: on an unclaimed pool slot it records nothing at all,
and the null surfaces hops later wherever it is used —

> `Employee.Single(e => e.User == decidedBy) matched no rows (Employee has 4 rows) — `decidedBy` was null.`

⚠ **And `Session.CurrentUser` is not the answer either** — it is refused inside a workflow body, because a body runs
on the engine's own authority and may resume after a park with nobody signed in (see [What a workflow body may write](https://osysharp.com/reference/workflow/body-security/)).
The compiler names `actor` when you write it.

**Claiming first is a real thing to do, but it is not how you get an actor.** Claim when you want the slot to *stop*
being a pool — to take it out of everyone else's queue while you work on it:

```osy title="claiming takes it out of the pool — a different intent from recording who acted" syntax
if (PoApproval.For(po).Legal.IsUnassigned) { PoApproval.For(po).Legal.Claim(); }
```

Claiming *in order to record somebody* would write down a falsehood: it registers whoever acted as the **assigned
approver**, which on a pool slot they never were, and on a slot assigned to someone else it is simply the wrong name.

A slot with an `Assignee` setting is handed to that person when it arms — so there `Assignee` answers, and `actor` is
still the one that says who actually acted. The two are different questions; see [actor — who just did this](https://osysharp.com/reference/workflow/actor/).

### May this person claim? Asking the rule yourself   {#asking}
A screen that offers a **Claim** button has to know whether the viewer may claim — and the only honest answer is the
slot's own rule. Ask it:

```osy title="asking a named run whether this person may claim" test app=workflow-candidates-ask
enum PoStage { Draft, Review, Done }
enum Dept { Legal, Finance }

[Principal] entity Person {
  [Required, MaxLength(80)] string Name;
  Dept Department = Dept.Legal;
  security { allow read, create when IsAuthenticated; }
}

entity Po {
  [Required, MaxLength(120)] string Title;
  PoStage Stage;
  [Required] Person Requester;
  security { allow read, create, update when IsAuthenticated; }
}

workflow PoApproval {
  Tracks    = Po.Stage;
  Autostart = true;
  Initial   = Draft;

  event Submit();
  event Approve();

  state Draft { subscribe Submit(); on Submit { goto Review; } }

  state Review {
    subscribe Approve() as Legal {
      // Four-eyes: a Legal approver, and never the person who raised it.
      Candidates = u => u.Department == Dept.Legal && u != this.Item.Requester;
    }
    on Legal { goto Done; }
  }

  terminal success Done { }
}

// What a screen asks before it draws a Claim button — the slot's OWN rule, not a second copy of it.
bool MayClaimLegal(Po po, Person who) {
  return PoApproval.For(po).Legal.Candidates(who);
}
```

It **inlines the declared predicate**, so there is one expression of the rule rather than two. That matters more than
it sounds: a page that re-types the rule drifts from the slot silently, and always in the worse direction — offering a
button the deposit then refuses, or hiding one that would have been accepted. Because it inlines rather than calling
the engine, it also lowers into the surrounding read, so it is legal in a `live var` and in a client-rendered page.

Inside a [milestone](https://osysharp.com/reference/workflow/milestone/) body the same question is `slot.Candidates(u)`, where the run is ambient
rather than named. Both forms answer identically, and both accept either declaration form — against a computation the
call becomes a membership test over the computed set.

**On a FANNED-OUT slot, naming the instance is what binds the fan-out variable.** A predicate like
`Candidates = u => u.Hat == h` reads the loop variable, so `.Architect.Candidates(u)` and `.Security.Candidates(u)`
ask two different questions from one declaration — the variable substitutes to that instance's own element.

⚠ **A DYNAMIC fan-out is refused**, because its slots have no static names — they are addressed at run time by the
acting principal, so there is no single slot for the question to be about. Use [`Workflow.Inbox<T>()`](https://osysharp.com/reference/workflow/inbox/)
there, which answers it for the caller.

⚠ **A slot that declares no `Candidates` is REFUSED here, not answered `true`.** Such a slot admits everyone, so the
question has no content, and a caller guarding on a constant is guarding on nothing:

```console
slot 'Legal' declares no `Candidates`, so every principal is eligible and `.Candidates(...)` has nothing to answer.
Drop the check, or declare `Candidates` on the slot.
```

⚑ **The check is a courtesy, never the gate.** Authorization happens at the deposit, server-side, whatever the screen
drew — so a page that offers the wrong button is a cosmetic bug rather than a security one. That is the right
direction, and it is why this may be used freely in UI.

## Examples       {#examples}
Eligibility as a rule — only a Legal approver who is not the requester (four-eyes):

```osy title="predicate: rule over the tested principal" syntax app=support-triage
subscribe Decide() as Legal {
  Candidates = u => u.Department == Dept.Legal && u != this.Item.Requester;
}
```

Eligibility as a computed set — the on-call agents in the ticket's region:

```osy title="computation: the eligible set off the run's data" syntax app=support-triage
subscribe Handle() as Owner {
  Candidates = () => Agent.Where(u => u.Region == this.Item.Region && u.OnCall).ToList();
}
```

## See also       {#see-also}
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the `subscribe` slot `Candidates` lives on
- [[Authorize] (event)](https://osysharp.com/reference/workflow/authorize/) — `[Authorize]` on an event: who may RAISE (contrast with who may HOLD here)
- [Assign — handing a slot to a named colleague](https://osysharp.com/reference/workflow/assign/) — `Reassign`: who may MOVE a slot. A third question again, and eligibility to hold work is
  deliberately not authority over it — a principal `Candidates` admits still cannot take a slot off its holder


---

<!-- https://osysharp.com/reference/workflow/correlate/ -->

# Correlation — finding a run by a business key

> Let an inbound event find its run by a key the sender already knows — a tracking number, an invoice reference, an external id — when the sender has never heard of your workflow and could not hold a callback link if you sent one. You declare the key on the event; the platform finds the run; your own endpoint decides who may speak.

<!-- id: workflow-correlate · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/correlate/ -->

## Summary        {#summary}
Some senders will never hold anything you gave them. A carrier POSTs parcel scans for its whole network. A payment
provider POSTs settlement notices for every merchant it serves. Neither was handed a link for your particular run,
and neither can be — they had already built their integration before you existed, and it is keyed on **their** id
for the thing: a tracking number, a reference.

`[CorrelateOn]` declares which key finds which run, so a message carrying nothing but that key lands in the right
place.

```osy syntax
[CorrelateOn(this.Item.TrackingNumber == trackingNumber)]
event Scanned(string trackingNumber, string location);
```

This is the one case [a callback URL](https://osysharp.com/reference/workflow/callback-url/) cannot cover, and the reason is worth stating exactly:
**a token IS an address.** One digest, one slot, one run — so it can never miss and it can never be ambiguous, which
is also why it can do nothing for a sender who was never given one. A business key is the opposite kind of thing: it
must be **searched for**, and the search can answer none, one, or many. Everything below follows from that.

## Signature      {#signature}
```osy title="the attribute and the verb, in placeholder notation" syntax
[CorrelateOn(this.Item.<Property> == <parameter>)]     // on the event — declares the key
<Wf>.Correlate<Event>(args…)                           // in app code — deposits by that key
```

⚠ **`Correlate<Event>` is ONE NAME, not a generic call.** The event's name joins the verb, so an event `Scanned` on
a workflow `ParcelDispatch` is called `ParcelDispatch.CorrelateScanned(…)` — angle brackets are this page's
placeholder notation, and writing `Correlate<Scanned>` is a compile error (`unknown identifier`). Osy# is a C#
superset, so the brackets read like a type argument; they are not one here.

```osy title="the real spelling — Correlate joins the event name" syntax
ParcelDispatch.CorrelateScanned(trackingNumber, "Depot Malmo");   // the real spelling
```

The attribute takes **exactly one equality**, between a property of the workflow's tracked entity and one of the
event's own parameters. Either order reads the same. The verb takes the event's arguments and returns a
`CorrelationOutcome`.

## Description    {#description}

### The key names both sides, and the compiler checks both   {#key}
`this.Item.TrackingNumber` is where the key lives on the tracked row; `trackingNumber` is which part of the message
carries it. Naming only the message and matching the property by name would make a rename compile clean and stop
correlating silently — which is the failure this attribute exists to avoid, not to reproduce.

It is read as a **key**, not evaluated as a condition: the platform turns it into a single indexed lookup. That is why
it must be one equality. A compound condition would have no key to be unique about and no lookup to be one query.

### The key must be `[Unique]`, and that is the whole answer to "what if two runs match?"   {#unique}
A business key that matches two runs has no honest answer at delivery time — depositing into both is wrong, picking
one is arbitrary, and refusing at 3am is a page. It is a **modelling** error, so it is caught where modelling errors
belong:

```osy syntax
entity Shipment {
  [Required, Unique, MaxLength(60)] string TrackingNumber;   // ← without Unique, the app does not compile
  ShipmentStatus Status;
}
```

Drop the `[Unique]` and the compile fails, naming the property.

### Your app owns the door; the platform owns the search   {#the-door}
There is no platform endpoint for this, deliberately. A correlated deposit swaps an unguessable 256-bit token for a
**guessable** business key — a tracking number is printed on the parcel — so the door cannot be anonymous the way a
callback URL safely can. But every source authenticates differently: an API key, an HMAC signature, mTLS, an IP
allowlist. A platform door would have to pick one (wrong for most callers) or accept anything (wrong for all).

So you declare your own route, check the caller however that caller requires, and then call the verb. Three things
follow, and they are all improvements rather than costs:

- the authorization decision is written where you can read it, in your own source;
- there is no second forever-URL — a generic inbound path is a contract with third parties who cannot be told it
  changed, which is exactly the problem [`map event`](https://osysharp.com/reference/workflow/migration/) had to solve for callback links;
- it matches every other seam: `Http.*` egress, `client` blocks, `[Page]` routes and `app.Apis` are all yours.

⚠ **Whatever credential you choose, never render it.** A screen that displays the carrier's key hands whoever can see
the screen the ability to forge every message that key signs. Rendering a credential is not a display decision; it is
a grant.

### The outcome is a value, because a miss is ordinary   {#outcome}
A sender POSTs its whole network's traffic at you, and most of it is nobody's business of yours. Raising for the
normal case would make every endpoint a `try`/`catch` — and, worse, would flatten the one distinction the caller
actually acts on:

| Outcome | Means | What to tell the sender |
|---|---|---|
| `Deposited` | matched one live run; the event went in | accepted |
| `NoSuchKey` | nothing here carries that key | **try again** — this is also what a race looks like (the notice beating the row that would match it), and what somebody else's key looks like; you cannot tell them apart, so do not pretend to |
| `NotRunning` | the key names a row, but its run is over | accepted, **stop** — retrying can never change it |
| `Ambiguous` | more than one live run on that row | a modelling error that got past the gate |
| `NoRule` | the event declares no `[CorrelateOn]` | a programming error |

A sender **retries**; that is how its queue works, not a failure mode to defend against. Answering every miss with
one "not found" means either their queue hammers you for ever, or they drop a message you wanted.

### What correlation does NOT change   {#unchanged}
Finding the run is the only thing that differs. After that it is an ordinary deposit: the event's
[[workflow-authorize|`[Authorize]`]] predicate, the state's slot-or-route dispatch, the per-run lock, and the version
pin are all exactly as they are for [`<Wf>.RaiseX`](https://osysharp.com/reference/workflow/raise/). A run still on an older revision binds the
event against the contract it started under.

⚠ **Your own rows are stale afterwards.** The deposit happens on the engine's context, so anything you were holding —
and anything you re-query, because the identity map answers with the same instance — shows pre-deposit values until
the call returns. The verb re-reads for you; the thing to know is that the values you had *before* it are not the
ones you have after.

## Examples       {#examples}

### A parcel scan finds its shipment   {#parcel-scan}
The carrier knows a tracking number and nothing else. Note that nothing in `RecordScan` names a run, a slot or a row.

```osy title="declaring the key and depositing by it" test app=workflow-correlate
enum ShipmentStatus { Booked, InTransit, Delivered }

entity Shipment {
  [Required, Unique, MaxLength(60)] string TrackingNumber;
  ShipmentStatus Status;
  [MaxLength(200)] string? LastSeenAt;
  security { allow read, create, update when IsAuthenticated; }
}

workflow ParcelDispatch {
  Tracks    = Shipment.Status;
  Autostart = true;
  Initial   = Booked;

  [CorrelateOn(this.Item.TrackingNumber == trackingNumber)]
  event Scanned(string trackingNumber, string location);

  state Booked {
    subscribe Scanned() as Collection;
    on Collection(string trackingNumber, string location) {
      this.Item.LastSeenAt = location;
      goto InTransit;
    }
  }

  state InTransit {
    subscribe Scanned() as Delivery;
    on Delivery(string trackingNumber, string location) {
      this.Item.LastSeenAt = location;
      goto Delivered;
    }
  }

  terminal success Delivered { }
}

// Your own endpoint. Check the caller FIRST — here the route's API key does it — then call the verb.
bool RecordScan(string trackingNumber, string location) {
  var outcome = ParcelDispatch.CorrelateScanned(trackingNumber, location);
  return outcome == CorrelationOutcome.Deposited;
}
```

### Telling the two misses apart   {#misses}
This is the shape worth copying: one branch per outcome, and a `Retry` flag the sender can act on.

```osy title="one branch per outcome, with a retry flag for the sender" syntax
var outcome = ParcelDispatch.CorrelateScanned(scan.TrackingNumber, scan.Location);

if (outcome == CorrelationOutcome.Deposited) {
  return new ScanReceipt { Accepted = true,  Retry = false, Message = "recorded" };
}
if (outcome == CorrelationOutcome.NoSuchKey) {
  return new ScanReceipt { Accepted = false, Retry = true,  Message = "unknown — try again later" };
}
return new ScanReceipt { Accepted = false, Retry = false, Message = "no longer in transit" };
```

A fuller version — the route, the receipt, and every outcome exercised by tests — is
`demo/wf-supplier-dispatch`.

## See also       {#see-also}
- [Callback URLs — letting an outsider complete one slot](https://osysharp.com/reference/workflow/callback-url/) — the other way in, for a sender you CAN hand a link to; a token is an address, a key is a search
- [Raising a workflow event](https://osysharp.com/reference/workflow/raise/) — depositing when you already hold the row, which is the ordinary case
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the slot the event satisfies once correlation has found the run
- [[Authorize] (event)](https://osysharp.com/reference/workflow/authorize/) — who may raise an event, which correlation does not bypass


---

<!-- https://osysharp.com/reference/workflow/flow-metrics/ -->

# Flow metrics — how long an item took, and how much was waiting

> Wall-clock lead time for a run, split into the part somebody was working it and the part it sat waiting — plus where that time went, state by state. Derived from what already happened, so it answers for items that finished long before anyone decided to measure.

<!-- id: workflow-flow-metrics · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/flow-metrics/ -->

## Summary        {#summary}
Two reads answer "how long did this take?" without declaring a deadline of any kind:

```osy syntax
var flow = Delivery.For(ticket).Flow;            // Lead / Touch / Wait
var where = Delivery.For(ticket).TimeInStates;   // where the lead time went, longest first
```

`Flow.Lead` is wall clock from start to finish. `Flow.Touch` is the part of it the run spent in states you declared
as [[#description|working]]. `Flow.Wait` is the rest — parked on a customer, blocked on a clarification, sitting in a
queue. That split is usually the whole question: *"six days, of which four waiting on the reporter"* says something
*"six days"* does not.

## Signature      {#signature}
```osy syntax
<Workflow>.For(entity).Flow           // FlowMetrics
<Workflow>.For(entity).TimeInStates   // List<StateTime>, longest first
```

**`FlowMetrics`** — `Lead`, `Touch`, `Wait` (all `TimeSpan`), `IsFinished` (`bool`), `StartedAt` (`DateTime`),
`FinishedAt` (`DateTime?`).

**`StateTime`** — `State` (`string`), `Total` (`TimeSpan`), `Visits` (`int`), `Accrues` (`bool`).

## Description    {#description}

### Declaring what counts as "working"   {#accrues}
`Touch` is the time the run spent in the states the workflow lists in `Accrues`:

```osy syntax
Accrues = [Building, InReview];
```

That is the same declaration the SLA clocks read, and deliberately so — "which states are work happening in" is one
fact about your process, not two. A workflow that lists **no** accruing states is ungated, exactly as its clocks would
run around the clock: every state counts, `Touch == Lead`, and `Wait` is zero.

`Touch + Wait == Lead` always holds exactly. `Wait` is derived by subtraction rather than summed from the other
states, so the three numbers can never drift apart by a few ticks and leave a reader wondering which to believe.

### ⚠ This is not an SLA, and it is not the same number   {#vs-sla}
A slot's SLA `Elapsed` — the "1h32m of the 4h" an [inbox](https://osysharp.com/reference/workflow/inbox/) row shows — is a different measurement,
and mixing them up will quietly give you wrong reports:

| | `Flow.Lead` | SLA `Elapsed` |
|---|---|---|
| measures | wall clock | budget consumed |
| under `ServiceHours` | unaffected | advances only in business hours |
| exists when | always | only if a `Within` was declared |
| a Friday 16:00 → Monday 10:00 ticket | **66 hours** | **2 hours** |

Both answers are right. They answer different questions, and only one of them is "how long did the customer wait".

### It works on items that finished before you asked   {#retroactive}
These come from the run's own lifecycle timeline, not from a clock ticking alongside it. So they answer for every run
you have ever executed — including the quarter you now want to report on but were not measuring at the time. That is
the one property a live counter could never acquire afterwards.

⚠ **The limit is your retention window, not the computation.** Reaped runs take their timelines with them, so a year
of delivery history needs a run retention that reaches back a year. Decide that before you need the data — it is not
recoverable later.

### Work still in flight   {#in-flight}
For a run that has not finished, `Lead` is the elapsed time **so far** and `FinishedAt` is null, so a board can show
ageing work. `IsFinished` is what stops a reader taking an in-flight number for a delivered one.

### Rework shows up as `Visits`   {#visits}
`TimeInStates` sums **every** visit to a state, and `Visits` counts them. A state with a four-day total and four
visits is a different process from one with a four-day total and a single visit, and the count is the only thing that
says which you have.

## Examples       {#examples}

A ticket that was worked, blocked on its reporter, then worked again:

```osy title="lead, touch and wait" test app=workflow-flow-metrics
enum TicketState { Building, AwaitingReporter, Shipped }

[Principal] entity Dev {
  [Required, MaxLength(100)] string Name;
  security { allow read when IsAuthenticated; allow create when IsAuthenticated || IsAnonymous; }
}

entity Ticket {
  [Required, MaxLength(200)] string Title;
  TicketState State;
  security { allow read when IsAuthenticated; allow create, update when IsAuthenticated || IsAnonymous; }
}

workflow Delivery {
  Tracks    = Ticket.State;
  Autostart = true;
  Initial   = Building;
  Accrues   = [Building];          // waiting on the reporter is NOT working

  event Ask();
  event Answer();
  event Ship();

  state Building {
    subscribe Ask()  as Blocked;
    subscribe Ship() as Done;
    on Blocked { goto AwaitingReporter; }
    on Done    { goto Shipped; }
  }

  state AwaitingReporter {
    subscribe Answer() as Unblocked;
    on Unblocked { goto Building; }
  }

  terminal success Shipped { }
}

string HowLong(Ticket t) {
  var flow = Delivery.For(t).Flow;
  var worst = Delivery.For(t).TimeInStates.First();
  return $"took {flow.Lead}, worked {flow.Touch}, waited {flow.Wait} — most of it in {worst.State}";
}
```

## See also       {#see-also}
- [ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/) — `Accrues` and the business-hours calendar, on the SLA side
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — deadlines and breaches, which are a promise rather than a measurement
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — the raw timeline these are derived from


---

<!-- https://osysharp.com/reference/workflow/audit/ -->

# For(entity).Audit

> Reads a running instance's lifecycle timeline — every transition, claim, deposit, reminder and refusal as an WorkflowAuditEntry you can filter and count. The artifact ops reads in a dispute, and the one a reminder is observed through.

<!-- id: workflow-audit · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/audit/ -->

## Summary        {#summary}
`<Workflow>.For(entity).Audit` returns the instance's lifecycle **timeline** as a `List<WorkflowAuditEntry>` — one row per
thing that happened to the run, oldest first. It is the record ops reads to explain an outcome, and the only way to
observe events that change no state, such as a fired reminder.

## Signature      {#signature}
```osy syntax
<Workflow>.For(<entity>).Audit   // → List<WorkflowAuditEntry>
```
Each `WorkflowAuditEntry` carries:

- **`Kind`** — an [[workflow-audit#kinds|AuditKind]] (`Transitioned`, `Claimed`, `Released`, `Reminded`, `Refused`, `Deposited`, …);
- **`Slot`** — the slot alias the event concerns (`"Legal"`), or nothing for a non-slot event;
- **`Actor`** — the principal that acted, or nothing for an engine-fired event (a reminder, a deadline);
- **`Message`** — a short reason carried by the event;
- **`At`** — when it occurred.

## Description    {#description}
The timeline is computed on read from the instance's recorded events; app code never writes it. Assign it to a local
and query it with ordinary list operations:

```osy title="the timeline of one run" test app=workflow-audit
enum ClaimStatus { Filed, Settled }

entity Claim {
  [Required, MaxLength(120)] string Reference;
  ClaimStatus Status;                 // no default: an autostarting workflow supplies the starting value
  // IsAnonymous is here because a [Test] body runs as an anonymous caller: without it the seed in the test below
  // is refused, and the example would document a create that cannot happen.
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow ClaimFlow {
  Tracks    = Claim.Status;
  Autostart = true;                   // …so a Claim has a run — and therefore a timeline — from the moment it exists
  Initial   = Filed;
  event Approve();
  state Filed {
    subscribe Approve();
    on Approve { goto Settled; }
  }
  terminal success Settled { }
}

// The timeline is computed on read from the run's recorded events — app code never writes it.
int RefusalsFor(Claim c) {
  var audit = ClaimFlow.For(c).Audit;
  return audit.Where(a => a.Kind == AuditKind.Refused).Count();
}
```

A **reminder** has no state change to see, so a test that a reminder fired asserts on the audit — the same artifact a
human would inspect — rather than on a bespoke hook. Tests and audits agree by construction: the assertion cannot pass
unless the thing ops would see actually happened.

### What kinds of event are recorded?        {#kinds}
`AuditKind` names what happened: `Transitioned` · `Claimed` · `Released` · `Assigned` · `Cancelled` · `Reminded` ·
`Breached` · `Held` · `Resumed` · `Retargeted` · `Refused` · `Deposited`.

## Examples       {#examples}
```osy title="asserting that a reminder fired" run app=workflow-audit
[Test]
void a_reminder_shows_up_on_the_timeline() {
  var c = new Claim { Reference = "C-1" };
  UnitOfWork.Commit();

  TestClock.Advance(TimeSpan.FromHours(2));

  // The assertion reads the same artifact ops would — not a bespoke test hook.
  var audit = ClaimFlow.For(c).Audit;
  Assert.Equal(0, audit.Where(a => a.Kind == AuditKind.Reminded).Count());
}
```

## See also       {#see-also}
- [TestClock.Advance](https://osysharp.com/reference/testing/clock-advance/) — advance time so a reminder / deadline fires into the timeline
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the slot whose reminders and refusals the timeline records


---

<!-- https://osysharp.com/reference/workflow/migration/ -->

# Migrating runs that are still in flight

> A deploy that renames or removes a workflow state leaves runs parked in it with nowhere to stand. A workflow migration says, per state, what happens to those runs — `keep;` for one that needs nothing, `goto <State>;` to move it, `terminate` to end it — and what happened to the slots and deadlines it was waiting on.

<!-- id: workflow-migration · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/migration/ -->

## Summary        {#summary}
A workflow run is not a request that finishes while you deploy — it is a thing that sits and waits, sometimes for
days. So when a deploy renames a state, the runs parked in the old one are still parked there, and the new version has
nowhere to put them.

A **workflow migration** answers that, state by state, in the same `.migration` a rename or removal already uses.
It is checked when you deploy, against both versions, so a file that could not be applied stops the deploy — rather
than surfacing days later as a run nobody can move.

## Signature      {#signature}
```osy syntax
migration OrderFlow v2 -> v3 {
  on Submitted       { keep; }                       // reviewed, and nothing to do — say so
  on AwaitingPayment { goto Reviewing; }             // where a run parked here now stands
  on Abandoned       { terminate cancel "no route"; } // nowhere to go: end it, and say why
  on Fulfilling      { keep; reenter; }              // …and runs parked MID-BODY here pick up the new body

  on Escalated {                                     // ordinary Osy# over the run's own row, beside the verb
    this.Item.Priority = 3;
    goto Reviewing;
  }
}
```

## Description    {#description}

### Why the platform cannot guess   {#why}
Most of a version move needs no help: the new version's `Submitted` is the old version's `Submitted`, and a parked run
is simply re-pointed at it. Everything that can be matched by name is matched by name — the state, each slot, each
deadline — and you write nothing.

What cannot be guessed is what a rename or a removal MEANT. `Waiting` disappearing and `Reviewing` appearing looks
exactly like `Waiting` disappearing, full stop, and the two want opposite handling. So a run parked in a state the new
version does not declare is left where it is and reported, with the verb that would answer it. Writing the verb is how
you say what you meant — there is no separate approval flag, here or in a schema migration.

### Every parkable state must appear   {#exhaustive}
A workflow migration is **exhaustive**: every state a run can be sitting in has to have an `on` handler, including the
ones nothing happened to. A state you left out and a state you never noticed read identically on the page, which is
exactly the mistake this prevents — so `keep;` exists to make "I looked at this, and it needs nothing" something the
file says out loud. Miss one and the deploy stops, naming the state.

`keep;` also asserts more than an identity `goto` would: it says the slots and the clocks on that state are untouched
too.

`keep;`, `goto` and `terminate` are mutually exclusive: a handler decides ONE fate for a run parked there. A handler
carrying two of them has not decided, and the deploy says so.

### A run parked MID-BODY: `reenter;`   {#reenter}
The verbs above answer *where a run stands*. A run parked inside `await Workflow.Run(…)`, `await saga.Run(…)` or a leg
join has a second question to answer: it is halfway through a **body**, holding a call stack and local variables, and
that position belongs to one version's tree.

**By default such a run stays on the version it started under** and finishes with that version's behaviour. That is
usually what you want — "runs started under v1 keep v1's behaviour, new ones get v2" — and it is always the safer
reading of a deploy, so it is what happens when a migration says nothing.

`reenter;` opts the state's mid-body runs into the new body:

```osy syntax
migration OrderSaga v2 -> v3 {
  on Fulfilling { keep; reenter; }
}
```

At the run's **next step boundary** — when the child it is waiting on finishes — the new body runs from the top.
Work already done is recognised where it is recorded: a `Workflow.Once` returns its memo, and a step whose child has
finished takes that child's outcome. Execution passes through everything already satisfied and stops at the first
thing that has not happened. Nothing is translated, and nothing is skipped.

`reenter` is **orthogonal to position**, which is why it composes instead of replacing. `keep` / `goto` / `terminate`
say where the run lands; `reenter` says how it gets there. A handler still needs one of the three, so `reenter;`
alone is refused — it never answered the question the exhaustiveness rule asks.

Two things `reenter` does not cover, both reported by name rather than guessed:

- **A park inside a loop.** The run holds a materialized item list and a cursor into it, and "which steps completed"
  does not describe that — re-entering would restart the loop at its first item. Those runs finish where they are.
- **A body the new version has no counterpart for.** Bodies are matched by where they are declared
  (`Fulfilling.enter`), so a renamed state or a removed hook reads as "that body is gone" — a rename only you can
  confirm, with `goto`.

⚠ **Re-entry replays the ordinary statements between the steps.** Assignments are idempotent and fine; creating a row
is not, so creating one *before* a wait is a compile error. Put the creation in the step that already records its
result — `Workflow.Once("make-step-a", () => new StepA { … })` — and it happens exactly once however often the body
re-enters. See [Workflows that outlive the code that started them](https://osysharp.com/reference/workflow/change-over-time/) for the full replay rules.

### Changing the run's own data     {#statements}
A verb says where a run *stands*. Often that is only half of what the change meant: a run pushed from `Escalated` to
`Reviewing` because the escalation route was retired usually needs its row to say so too.

So a handler can carry **ordinary Osy# over `this.Item`**, beside the verb:

```osy syntax
migration OrderFlow v1 -> v2 {
  on Escalated {
    this.Item.Priority = 3;
    this.Item.Note = "escalation route retired in v2";
    goto Reviewing;
  }
  on Draft { keep; }
}
```

`this.Item` is the run's tracked row — the same `this.Item` every handler body of that workflow already has. Anything
you can write about it in an `enter {}` you can write here.

**The statements run FIRST, then the verb settles where the run stands.** That is the only order the example reads
in: `this.Item.Priority = 3; goto Reviewing;` is one instruction about a run that is still standing where its author
said. Where the statements appear among the verbs makes no difference — they are collected in the order you wrote
them and run as one block.

**It is one transaction with the move.** If a statement fails, nothing is written at all: the run keeps its version,
its position and its row, and the failure is reported against that run like any other refusal. There is no state where
half a migration has been applied.

**Written against the version you are deploying, and checked when you deploy it.** A property the new version does not
declare stops the deploy, naming it — not three days later against a parked run nobody is watching.

#### What a body may not do   {#body-limits}
Three things, each because the body runs while the run is *between* versions — not executing, and holding its own lock
inside the migration's transaction:

- **It cannot wait.** No `await Workflow.Run`, no `saga.Run`, no join. There is nothing for a park to return to. If
  work in flight should pick up the new version's behaviour, that is `reenter;` and the workflow body.
- **It cannot use a durable step or a saga.** `Workflow.Once` records itself against the *run's* step memo, and these
  statements belong to a version pair rather than to the run's execution — it would either collide with a real step or
  record one nothing can find again.
- **It cannot `goto`.** The handler already says where the run lands, in a verb the platform can read without running
  anything. Two answers to one question is how they come to disagree.

Each of these is refused when you deploy, and each message names the form that does work.

### Changing an event's SIGNATURE: `map event`     {#map-event}
A `slot.CallbackUrl()` is in somebody else's inbox. They have no account, no way to learn you redeployed, and nobody
can recall the link — so the body that arrives next week is shaped for the version the URL was *minted* under.

If you change that event's parameters, say what the old shape means under the new one:

```osy title="a dropped parameter, and an added one needing a value" syntax
migration OrderFlow v1 -> v2 {
  map event Approve(decision, reason) -> Approve(decision);                    // a parameter dropped
  map event Submit(amount)            -> Submit(amount, source = "callback");  // an added one needs a value
  on AwaitingApproval { keep; }
}
```

**It sits at the migration's top level, not inside an `on`**, because a signature belongs to the EVENT: the same event
can be waited on by slots in several states, and a per-state spelling would repeat itself and could contradict itself.

**Parameters match by NAME**, never by position — a migration is read a year later by somebody who was not there, and
"the second one" is not a fact anybody can check. Write the old signature out on the left; it is checked against what
that version actually declared, because it is the record of the contract the links already sent out were minted
against. A parameter the old signature cannot supply takes a literal: `source = "callback"`.

**The event's name is the same on both sides.** This verb maps a signature. A *renamed* event is a different event, and
the slot that waits on it is re-pointed with `rename slot`.

#### It is REQUIRED when a signature moves   {#map-required}
A deploy that changes an event's parameters and says nothing about it stops, naming the event and spelling the verb.
There is no safe default: the platform cannot invent a value for a parameter that did not exist, and it cannot decide
that a dropped one did not matter.

#### What happens when the link comes back   {#map-callback}
The token records the version it was minted under. When the body arrives, it is read against THAT version's signature
and then walked forward through each deploy's mapping, in order, into the signature the run is on now.

Walked rather than composed on purpose: a failure can say *which* hop's contract broke, which is what you tell the
third party still holding the URL. A hop that says nothing about the event passes the payload through unchanged —
that is exactly the statement that its signature did not move.

### What a `goto` moves   {#goto}
`goto <State>;` re-points where the run is parked. It does not re-run anything: no enter body, no route, no code of
yours. What moves with it is everything that names the run's position —

- the run's **current state**, resolved against the version being deployed;
- the **tracked property** on the entity (`Tracks = Job.Stage`), so a query, a grid or a page reads the same answer
  the run does. The state you send it to must be a member of that enum, or the deploy stops;
- the **wait** the run is standing at: an open slot on the state it left becomes the same-named slot on the state it
  is sent to, so a claim, an assignment, or an outstanding callback URL keeps working across the move;
- an entry on the run's **timeline**, so its history shows the move instead of skipping over it.

What does NOT move on its own is elapsed time. A clock that has been running goes on running — a migration re-points a
run, it does not restart it. The one exception is a deadline whose BUDGET you changed, which you have to decide about;
see *Deadlines* below.

A `goto` target must be a state of the version you are deploying.

### Ending runs that have nowhere to go   {#terminate}
Sometimes there is no honest answer to "where does this run stand now" — the route it was waiting on is gone, and no
state in the new version means what its old one meant. `terminate <outcome> "<why>";` says that out loud:

```osy title="ending a run that has nowhere honest to go" syntax
on Abandoned { terminate cancel "the offline-payment route was removed in v3"; }
```

The run **ends where it stands**, on the version it is already on. Nothing is re-pointed, because a finished run never
resolves its definition again and its history describes itself. It is left `Completed`, `Cancelled` or `Failed` to
match the outcome you named — the same three a `terminal` state can produce — its deadlines are retired, and your
sentence goes on its timeline, where whoever finds it tomorrow will look.

The reason is required. This is the one verb that ends work somebody was waiting on, and a run that stopped for no
recorded reason is the kind of thing that gets escalated a week later with nobody able to answer it.

A migration pass reports ended runs SEPARATELY from moved ones. "We ended forty runs" is not a variety of "we moved
forty runs", and you should never have to read the difference out of a state name.

### Slots: the wait itself   {#slots}
A state's `on` handler also says what happened to the **slots** that state waits on — a different question from where
the run goes, so these sit beside `keep`/`goto`/`terminate` rather than instead of them.

```osy title="renaming a wait, and cancelling one that is gone" syntax
on AwaitingApproval {
  keep;
  rename slot Payer -> Payee;      // the same wait, under a new name
  drop slot LegacyApprover;        // gone in the new version: cancel the claim, explicitly
}
```

A slot renamed past the automatic name-match needs `rename slot`, because from the outside a renamed slot and a
removed one look identical. The verb is required in front of the names: a bare `Payer -> Payee` sitting next to
`drop slot X` would leave the reader to work out which kind of thing is being changed.

`drop slot` cancels a **live claim**, which is why it has to be said rather than inferred. The slot is marked
cancelled and its deadlines are retired — and its callback token is cleared, so a URL somebody is still holding stops
working. That is the one place a minted callback URL is meant to stop working; everywhere else it survives a
migration, because it is addressed to the run and the slot, not to a version.

Both are checked when you deploy: the slot has to exist under that name in the version being replaced, and a rename's
new name has to exist on the state the run lands in. And a handler that drops **every** wait a state has, while saying
the run stays there, is refused — that would park it where nothing could ever advance it, so it has to say where the
run goes instead. The same check runs per run at drain time, for the case only the run knows: the other slots exist,
but this particular run had already satisfied them.

### Deadlines: the one thing that has no safe default   {#deadlines}
A run parked at a wait is usually counting against a deadline — an `Assigned` race to take the work, a `Finished`
budget to do it. Those are re-pointed for you, like everything else that can be matched: a deadline is identified by
its kind on the slot it hangs off, so it survives a rename of either.

What is re-pointed is also **re-read**. If you changed the budget, the run gets the new one — a clock that pointed at
the new declaration while still counting the old number would fire at a time no source anywhere states.

That leaves a question only you can answer, and it is the reason this verb exists:

```osy title="a moved SLA budget: carry the elapsed time, or reset it" syntax
on AwaitingApproval {
  keep;
  carry clock Finished on Payee;   // keep the time already spent, against the new budget
  // reset clock Finished on Payee;  // or: start the new budget from now
}
```

Suppose a run has spent three hours of a four-hour SLA and you shorten it to two. **Carrying** the elapsed time puts
that run instantly past its deadline — deploy once, breach every live SLA at the same moment. **Resetting** hands it a
fresh two hours and quietly forgives three that really passed, so a breach that happened stops being visible. Neither
is a default anyone would want applied silently, so a deploy that moves a live budget and says nothing about it is
**refused**, naming the timer, both budgets, and both spellings.

The kind is `Assigned` or `Finished`, and the slot is named as the version being **replaced** spells it — the same
rule the slot verbs follow, because that is the only version a parked run's wait exists in.

You only write this when a budget MOVED. Everything else about a deadline is handled for you:

| what you did to the declaration | what happens to a parked run's timer |
|---|---|
| left it alone | carries — same budget, same accrued time |
| **changed the budget** | **you say: `carry` or `reset`. The deploy stops until you do.** |
| moved it between a slot, its state and the workflow | carries. It is the same deadline, one level out |
| added one | arms when the run reaches it, like any new run |
| **removed it** | **retired.** The run keeps going with one fewer obligation |

**Moving a declaration between levels changes nothing.** A `Finished` block on a slot, on its state, or on the
workflow as a default is the same obligation written at three levels of reach; each slot takes the nearest one that
applies to it, and that is settled when you compile. So tidying `Finished { Within = 4h; }` off three slots and onto
the state they share is not a change any parked run can see, and needs no verb.

⚠ **With one edge worth knowing, because it does not look like a deadline change at all.** Only a **pool** slot — one
that declares `Candidates` — inherits a deadline from its state or its workflow; a plain `subscribe Submit();` is not
SLA-tracked and takes nothing from above it. So if a slot was relying on an inherited SLA and you remove its
`Candidates`, you have removed its deadline too, and parked runs have that timer **retired**. The report and the
timeline both say so, but the line you edited was about who can claim the work.

**Removing a deadline retires it, and needs no verb either.** Unlike a budget that moved, deleting a declaration has
only one reading: the promise was withdrawn. So the timer stops, the run carries on, and nothing breaches — a run
that can no longer be late is not a run that failed. The alternative would be to refuse the move and strand somebody's
work over a deadline you deliberately deleted.

Automatic is not the same as invisible. A retired deadline is recorded in three places: `osy migrate` reports how many
stopped and which, the run's own timeline gets a **`Retired`** entry naming the promise and the budget it was running
under, and `compile --generate-migration` says so in the file when it is writing one anyway. It is its own timeline
kind rather than a deadline "moved to zero", because zero would mean *breached this instant* — the opposite of what
happened, and a timeline is read long after anyone remembers the deploy.

A **reminder** removed from a milestone that still exists is the same thing one level in: that nudge stops, and the
SLA it hung off keeps running.

`osy compile --generate-migration` spots a moved budget exactly and still will not choose for you: it writes the move
and both spellings, commented out, so the file does not deploy until you uncomment one.

### Fan-out slots: keep the form   {#fan-out}
A slot that fans out — one wait per element — migrates like any other, with its per-element slots carried across. What
it cannot survive is a change of FORM: fanning out over a fixed list of enum members and fanning out over a runtime
collection are addressed differently (by the member's name, and by whoever is acting), so the keys a parked run's
slots carry mean nothing to the other. Nothing can translate them — a member name is not a row.

So a deploy that flips the form, or that makes a plain wait fan out (or the reverse), **refuses the runs holding
those slots**, naming the slot and which way it changed. They stay on the version they were parked on, where they
still work. Keep the form for runs in flight, or end them deliberately with `terminate`.

### Several deploys at once, in order   {#multi-hop}
A run can be several versions behind — it was parked while three deploys went out. Each version PAIR carries its own
instruction, and they are applied **in order**, one hop at a time: v1's migration first, then v2's, then v3's.

That is not a detail. The verbs are cumulative: a `goto` in the first hop decides which state the second hop's
handlers apply to at all. A run parked in `Waiting`, with `v1 -> v2` saying `goto Reviewing;` and `v2 -> v3` saying
`goto Approved;`, lands in **`Approved`** — not in `Waiting`, and not nowhere. A deploy that carried no migration is
simply a hop with nothing to say, so a mixed history needs nothing special from you.

The instruction is kept by the platform when you deploy, so it is still there when the run finally drains — which may
be long after the deploy that carried the file. Your `.migration` stays where it belongs: in source control.

### The header names the two versions   {#header}
`migration OrderFlow v2 -> v3` names the workflow and the version numbers this file spans. They are checked: a file
written for one pair of versions refuses to be applied to a different one, the same guard a schema migration's
`from`/`to` gives you. `osy versions` shows which version an app is on.

## Examples       {#examples}
The deployed version, with a run parked in `AwaitingPayment`:

```osy title="deployed" test app=workflow-migration-before
enum Stage { Draft, AwaitingPayment, Done }

entity Order {
  [MaxLength(60)] string? Reference;
  Stage Stage = Stage.Draft;
}

workflow OrderFlow {
  Tracks = Order.Stage; Autostart = false; Initial = Draft;
  event Submit();
  event Settle();
  state Draft           { subscribe Submit(); on Submit { goto AwaitingPayment; } }
  state AwaitingPayment { subscribe Settle(); on Settle { goto Done; } }
  terminal success Done { }
}
```

The version you are about to deploy, where that state has been renamed:

```osy title="deploying" test app=workflow-migration-after
enum Stage { Draft, AwaitingSettlement, Done }

entity Order {
  [MaxLength(60)] string? Reference;
  Stage Stage = Stage.Draft;
}

workflow OrderFlow {
  Tracks = Order.Stage; Autostart = false; Initial = Draft;
  event Submit();
  event Settle();
  state Draft              { subscribe Submit(); on Submit { goto AwaitingSettlement; } }
  state AwaitingSettlement { subscribe Settle(); on Settle { goto Done; } }
  terminal success Done { }
}
```

The `.migration` that carries the parked runs across, one line per state:

```osy title="the migration file — one line per parked state" syntax
migration OrderFlow v2 -> v3 {
  on Draft           { keep; }
  on AwaitingPayment { goto AwaitingSettlement; }
}
```

A route the new version removed, whose parked runs are ended rather than left waiting on an event nobody will ever
raise:

```osy title="ending parked runs instead of leaving them waiting" syntax
migration OrderFlow v2 -> v3 {
  on Draft     { keep; }
  on Abandoned { terminate cancel "the offline-payment route was removed in v3"; }
}
```

What you see when a state is left out:

```text
'OrderFlow' v2 -> v3 does not say what happens to a run parked in 'Abandoned'. Every parkable state must appear —
say `keep;` where nothing is needed, so a reviewed state and a missed one never look alike.
```

## See also       {#see-also}
- [Workflows that outlive the code that started them](https://osysharp.com/reference/workflow/change-over-time/) — the model underneath these verbs: how a run's position survives an edit at all
- [Renaming and removing things that hold data](https://osysharp.com/reference/project/renaming-and-removing/) — the same file's other half: what happens to the DATA a rename or removal touches
- [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/) — what a version is, and which runs are still holding an old one open
- <span class="planned" title="this page is planned and not written yet">workflow-state</span> — the states a migration addresses
- [Tracks and Initial (the field a workflow drives)](https://osysharp.com/reference/workflow/tracks/) — the tracked property a `goto` keeps in step with the run
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the slots a `rename slot` / `drop slot` addresses
- [Callback URLs — letting an outsider complete one slot](https://osysharp.com/reference/workflow/callback-url/) — the token `drop slot` clears, and that every other verb keeps working


---

<!-- https://osysharp.com/reference/workflow/parallel-legs/ -->

# Parallel legs (start several, then wait for them)

> Start several pieces of work at once, each with its own compensation, and wait for them together. Writing `saga.Run("step", ...)` WITHOUT `await` starts a leg and hands back a handle; `await Workflow.WhenAll(a, b)` waits for every leg, `WhenAny` for the first to succeed, `When(n, …)` for enough of them. Each leg is a different kind of work — a flight and a hotel, not ten of the same thing — so each is created by ordinary code and carries the undo that belongs to it.

<!-- id: workflow-parallel-legs · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/parallel-legs/ -->

## Summary        {#summary}
Some work has to happen **together**. Booking a trip means a flight *and* a hotel; neither is worth having on its own,
both take time, and doing them one after the other means the customer waits twice as long for an answer.

Inside a [Workflow.BeginSaga (a compensating saga scope)](https://osysharp.com/reference/workflow/beginsaga/) scope, **a `saga.Run("step", ...)` written without `await` starts a leg and carries on**. It
hands back a **leg handle**, and `await Workflow.WhenAll(...)` waits for the legs you name. The distinction is the same
one C# draws between `var t = Foo();` and `await Foo();` — start, versus start and wait.

Each leg keeps everything that made a sequential step readable: it is created by ordinary statements above the call,
and its compensation sits on the call that starts it. Nothing has to be squeezed into an argument.

When the legs *are* many of the same kind — one per order line, say — **build the list yourself and hand it to the same
wait**: `await Workflow.WhenAll(legs)`. You still write the loop, the creation and the undo; only the count stops being
something the source knows.

## Signature      {#signature}
```osy syntax
var leg = saga.Run("step", step, () => Undo(step));   // START a leg — no `await`

await Workflow.WhenAll(a, b, c);              // every leg
await Workflow.WhenAny(a, b, c);              // the first to succeed
await Workflow.When(2, a, b, c);              // enough of them

await Workflow.WhenAll(legs);                 // …or a LIST of legs, when the count is only known at run time
await Workflow.When(2, legs);

await Workflow.WhenAny(a, b, losers: Losers.Continue);   // …and leave the rest running
```

`await` is what separates the two forms, and it is not decoration:

| you write | what happens |
|---|---|
| `await saga.Run("step", step, undo)` | run this one step and **wait** for it before the next line |
| `var leg = saga.Run("step", step, undo)` | **start** it and carry on; `leg` is what you wait on later |

## Description    {#description}

### Every leg you start must be joined   {#must-join}
A leg's compensation is registered **when the leg succeeds**, and the wait is what learns that. So a leg you start and
never wait for is work with nothing to undo it — a hotel booked with no way back. That never compiles: the compiler
names the leg and the line.

For the same reason, a wait must be written with `await`. Without it nothing waits, and nothing observes the outcomes.

### Many legs of the same kind: build the list, pass the list   {#list-of-legs}
Sometimes the legs really are the same work repeated — reserve every line of this order, notify every subscriber — and
how many there are is a property of the data, not of the source. Build them in a loop and hand the wait the list:

```osy syntax
var legs = new List<Leg>();
foreach (var line in order.Lines)
  legs.Add(saga.Run("reservation", new Reservation { Line = line }, () => Release(line)));

await Workflow.WhenAll(legs);
```

This is the same overload pair C# gives `Task.WhenAll` — several tasks, or a sequence of them — which is why it is an
argument form rather than a fourth verb. **Everything that made a leg readable stays where it was**: you write the
loop, you create each step, and each carries its own undo. The wait is handed handles; it does not iterate on your
behalf and it does not own any leg's body.

Everything below applies per element: the settle-then-unwind rule, which legs get compensated, and the requirement
that every leg you start is joined — a list built and never waited for is refused by name, exactly as a single leg is.

### Waiting for enough, not for all   {#whenany}
`WhenAll` waits for every leg. `WhenAny` returns as soon as **one succeeds**, and `When(n, …)` as soon as **n do** —
they count successes, not finishes, because the question is "did enough of my work get done", and a leg that failed
did not.

If a quorum becomes impossible — too many legs have failed — the join reports the failure once every leg has settled,
exactly as `WhenAll` does.

### What happens to the legs that lost   {#losers}
By default they are **cancelled and compensated**. A leg you gave an undo to is one you said must not dangle, and work
left standing with nothing to reverse it is the failure that is invisible until it matters.

Cancelling is thorough: the leg stops, **and every level of it unwinds itself on the way out**, so a leg that had its
own compensations registered runs them before it ends. Then this run's own undo for that leg runs too.

That last part is worth being precise about, because it is the difference between correct and nearly correct: a
loser's compensation **runs now**, rather than joining the stack that unwinds if the saga fails. A saga that goes on to
`Complete()` drops that stack — so a loser parked there would be quietly forgotten by exactly the run that won.

Say `losers: Losers.Continue` when the losers really are irrelevant rather than wrong — a "first responder answers"
race where the others doing their work harms nothing. It has to be written down; it is not somewhere you should be
able to arrive by omission.

`WhenAll` has no losers, so passing `losers:` there is an error rather than something quietly ignored.

### What `WhenAll` guarantees   {#whenall}
- **It returns only when every leg has finished.** One leg finishing changes nothing on its own.
- **Every leg that succeeded is compensable.** Their undos go onto the scope's stack in the order the legs were
  started, so a later failure unwinds them last-started-first — the same order a sequence of steps produces.
- **A leg that failed compensates nothing.** Only work that actually committed gets an undo. This is the same rule a
  sequential step follows, and it is why the wait, rather than the start, is where undos are registered.

### When a leg fails: everything settles first   {#settling}
If one leg fails while another is still running, **nothing is cancelled and nothing is compensated yet**. The wait lets
the other legs reach their own conclusion, and only then reports the failure.

This is deliberate, and it is the difference between a compensation that is safe and one that is not. Compensating
against a leg that is *halfway through* means undoing something whose extent nobody knows — the hardest problem in the
category. Letting each leg finish first means every compensation faces a settled fact.

The cost is latency on a path that was already failing, which is the cheapest place to spend it.

### What it costs you when it fails   {#failure}
The failure arrives as an ordinary catchable workflow error naming the legs that failed, so the surrounding
`try`/`catch` — and the scope's unwind — work exactly as they do for a single step. Nothing new to learn.

## Examples       {#examples}
The world the trip is booked in — two kinds of work, each its own entity, its own workflow, and its own way back:

```osy title="the app the trip saga lives in" test app=wf-parallel-legs-example
enum TripStatus { Planning, Booked, Abandoned }
enum LegStatus  { Pending, Confirmed }

entity Trip   { [Required, MaxLength(60)] string Destination; TripStatus Status = TripStatus.Planning; }
entity Flight { [Required] Trip Trip; [Required, MaxLength(20)] string Route; LegStatus Status = LegStatus.Pending; }
entity Hotel  { [Required] Trip Trip; [Required, MaxLength(60)] string City;  LegStatus Status = LegStatus.Pending; }

void CancelFlight(Flight flight) { }
void CancelHotel(Hotel hotel)    { }

workflow FlightFlow { Tracks = Flight.Status; Autostart = false; Initial = Pending;
  event Confirm(); state Pending { subscribe Confirm(); on Confirm { goto Confirmed; } } terminal success Confirmed { } }
workflow HotelFlow { Tracks = Hotel.Status; Autostart = false; Initial = Pending;
  event Confirm(); state Pending { subscribe Confirm(); on Confirm { goto Confirmed; } } terminal success Confirmed { } }
```

Then the booking itself — both legs in flight at once, each with the undo that belongs to it:

```osy title="book the flight and the hotel together" test app=wf-parallel-legs-example
workflow TripFlow {
  Tracks = Trip.Status; Autostart = false; Initial = Planning;
  state Planning {
    enter {
      await using var saga = Workflow.BeginSaga();
      try {
        // Each row is created inside a `Workflow.Once` step: the body is re-entered from the top after a wait, so a
        // bare `new` would create a second row on resume. Complex creation is just statements.
        var flight = Workflow.Once("make-flight", () => new Flight { Trip = this.Item, Route = "LHR-JFK" });
        var hotel  = Workflow.Once("make-hotel",  () => new Hotel  { Trip = this.Item, City  = this.Item.Destination });

        var bookingFlight = saga.Run("flight", flight, () => CancelFlight(flight));  // STARTED, not waited for
        var bookingHotel  = saga.Run("hotel", hotel,  () => CancelHotel(hotel));

        await Workflow.WhenAll(bookingFlight, bookingHotel);               // …now wait for both

        saga.Complete();
        goto Booked;
      } catch (WorkflowError e) { goto Abandoned; }   // whichever leg stood is cancelled on the way out
    }
  }
  terminal success Booked     { }
  terminal error   Abandoned  { Message = "trip abandoned"; }
}
```

If the hotel cannot be had, the flight leg is still allowed to finish — and *then* the flight is cancelled and the trip
routes to `Abandoned`. If both stand, `saga.Complete()` drops the undos and nothing is cancelled.

Change one word and it becomes a race — book whichever is available first, and release the other:

```osy title="whichever confirms first wins" test app=wf-parallel-legs-example
void ReleaseFlight(Flight flight) { }
void ReleaseHotel(Hotel hotel)    { }

workflow RaceFlow {
  Tracks = Trip.Status; Autostart = false; Initial = Planning;
  state Planning {
    enter {
      await using var saga = Workflow.BeginSaga();
      try {
        var flight = Workflow.Once("make-flight", () => new Flight { Trip = this.Item, Route = "LHR-JFK" });
        var hotel  = Workflow.Once("make-hotel",  () => new Hotel  { Trip = this.Item, City  = this.Item.Destination });

        var byAir  = saga.Run("flight", flight, () => ReleaseFlight(flight));
        var byRoom = saga.Run("hotel", hotel,  () => ReleaseHotel(hotel));

        await Workflow.WhenAny(byAir, byRoom);   // the loser is cancelled AND released
        saga.Complete();
        goto Booked;
      } catch (WorkflowError e) { goto Abandoned; }
    }
  }
  terminal success Booked    { }
  terminal error   Abandoned { Message = "trip abandoned"; }
}
```

And when the legs are one-per-element, the loop is yours and only the wait changes:

```osy title="reserve every line of the order, in parallel" test app=wf-parallel-legs-list
enum OrderStatus { Placed, Reserved, Abandoned }
enum LineStatus  { Pending, Held }

entity Order {
  [Required, MaxLength(30)] string Ref;
  OrderStatus Status = OrderStatus.Placed;
  [ForeignKey(Order)] OrderLine[] Lines;
}
entity OrderLine   { [Required] Order Order; [Required, MaxLength(20)] string Sku; int Quantity; }
entity Reservation { [Required] OrderLine Line; LineStatus Status = LineStatus.Pending; }

void ReleaseReservation(Reservation reservation) { }

workflow ReservationFlow { Tracks = Reservation.Status; Autostart = false; Initial = Pending;
  event Confirm(); state Pending { subscribe Confirm(); on Confirm { goto Held; } } terminal success Held { } }

workflow OrderFlow {
  Tracks = Order.Status; Autostart = false; Initial = Placed;
  state Placed {
    enter {
      await using var saga = Workflow.BeginSaga();
      try {
        var legs = new List<Leg>();
        foreach (var line in this.Item.Lines) {
          // The step id carries a per-iteration segment, so one label inside a loop is one step PER LINE.
          var reservation = Workflow.Once("make-reservation", () => new Reservation { Line = line });
          legs.Add(saga.Run("reservation", reservation, () => ReleaseReservation(reservation)));   // one leg per line
        }

        await Workflow.WhenAll(legs);          // however many that turned out to be

        saga.Complete();
        goto Reserved;
      } catch (WorkflowError e) { goto Abandoned; }   // the lines that WERE held are released on the way out
    }
  }
  terminal success Reserved  { }
  terminal error   Abandoned { Message = "order abandoned"; }
}
```

## Notes          {#notes}
- **The legs are named by your variables.** A failure says which leg failed, because you gave it a name — not "leg 2".
- **A leg is a workflow of its own**, so it can wait on people, timers and events exactly as any workflow does. That is
  the case the wait exists for; legs that finish instantly work too, and simply never park anything.
- **Starting a leg does not wait for anything**, so the statements between the starts run immediately. The single pause
  in the whole shape is the wait itself.
- **The count in `When(n, …)` is a plain number**, not an expression. A join that asks for more legs than it was given
  can never be satisfied — a compile error when you passed the legs one by one, and a fault at the wait itself when
  they came from a list, which is the first moment the count exists.
- **A list of legs is still your legs.** The wait never iterates anything and never creates anything; it is handed
  handles you made. If you want the platform to fan work out over a collection *for* you, that is a different
  question — see [dynamic fan-out (foreach over a runtime collection)](https://osysharp.com/reference/workflow/fan-out-dynamic/).

## See also       {#see-also}
- [Workflow.BeginSaga (a compensating saga scope)](https://osysharp.com/reference/workflow/beginsaga/) — the scope these legs live in, and how compensation unwinds.
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — start one workflow, and optionally wait for it.
- [dynamic fan-out (foreach over a runtime collection)](https://osysharp.com/reference/workflow/fan-out-dynamic/) — many instances of the *same* work, one per element.


---

<!-- https://osysharp.com/reference/workflow/raise/ -->

# Raising a workflow event

> Raise a typed event on the run bound to an entity, from anywhere — an ordinary server function, a webhook handler, a signup step. Two spellings do it: the NAMED form states which workflow, and the INFERRED form works it out from the entity's type, which is what generic code wants when it should not have to know.

<!-- id: workflow-raise · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/raise/ -->

## Summary        {#summary}
An event is how the outside world moves a workflow along. Raising one advances the run bound to an entity — routing
to the slot in the current state that subscribes it, or to a workflow-level arm that handles it wherever the run has
got to.

There are two spellings, and the only difference is whether the call **names** the workflow:

```osy syntax
OrderFlow.RaisePayment(order, 100);       // NAMED    — you say which workflow
Workflow.Raise(order, Payment(100));      // INFERRED — worked out from the entity's type
```

Both drive the same engine, enforce the same [[workflow-authorize|`[Authorize]`]] gate, and route the same way.
Neither is a test-only surface: an ordinary server function may raise an event, and that is the normal way an app
advances a run from outside the workflow.

## Signature      {#signature}
```osy syntax
<Workflow>.Raise<Event>(entity, args…)    // named: the method is the workflow's, one per declared event
Workflow.Raise(entity, <Event>(args…))    // inferred: the event is written as a constructor call
```

`entity` is the row the run is bound to — whatever the workflow [`Tracks`](https://osysharp.com/reference/workflow/tracks/). The arguments are the
event's declared parameters, in order, and they arrive in the arm **by name**.

## Description    {#description}

### Which spelling to use   {#spellings}
Reach for the **named** form by default. It is the one that reads back as what it does — the workflow is on the page,
so anyone changing the model can find every producer of an event by searching for it, and nothing about the call
depends on facts elsewhere in the model.

Reach for the **inferred** form when the caller genuinely should not know the workflow: shared helpers, generic
plumbing, anything written against "an entity that has a workflow" rather than against one particular flow. It is
narrower than it looks — most application code knows perfectly well which workflow it is advancing, and writing that
down costs one identifier.

### ⚠ The inference needs the entity's type to have exactly ONE workflow   {#inference}
The inferred form resolves the workflow from the entity's **type**. If two workflows bind that type, there is nothing
to infer, and the compiler refuses the call rather than picking one:

```text
Workflow.Raise: more than one workflow in this unit binds 'Ticket', so the workflow cannot be inferred from the
entity — name it instead: `<Workflow>.RaiseResolve(t)`
```

This is a **compile** error on purpose, and the reason is the same as the reason the form exists. Generic code is
precisely the caller that cannot check: a helper holding somebody else's entity has no way to notice that the type
has since acquired a second workflow. So the check belongs where the whole model is in view. Adding a second workflow
to a type is a change that will name every inferred call that has just become ambiguous, and each is a one-word fix.

### Where it routes   {#routing}
Raising does not name a slot. The engine takes the event to:

- the slot in the run's **current state** that [subscribes](https://osysharp.com/reference/workflow/subscribe/) it — the ordinary case; or
- a **workflow-level** arm for it, wherever the run has got to — how a `Cancel` reaches a run in any state.

An event no slot is waiting for and no workflow-level arm handles is **refused**, not queued. To answer a specific
queued slot from a person's inbox — where the row, not the code, decides which slot is being answered — use
[`Workflow.Deposit(row, …)`](https://osysharp.com/reference/workflow/inbox-act/) instead.

### The run has to exist   {#run-required}
Both forms raise on the run **already bound** to the entity: they do not start one. If nothing has started a run for
that row, the call fails saying so — [`Workflow.Run(entity)`](https://osysharp.com/reference/workflow/run/) (or `Autostart`) is what creates it.
When a row has more than one run over its lifetime, a run still **waiting** is preferred over a finished one.

### Authorization is the workflow's, not the caller's   {#authorization}
An event's `[Authorize]` predicate and a slot's `Candidates` are enforced by the engine, so they hold identically for
both spellings and for a raise from ordinary application code. Being able to call the function is not permission to
advance the run.

## Examples       {#examples}

An ordinary server function advancing a run — the signup / webhook shape, with the workflow named:

```osy title="named" test app=workflow-raise-named
enum TicketStatus { Working, Closed }

entity Ticket {
  [Required, MaxLength(200)] string Title;
  TicketStatus Status = TicketStatus.Working;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow TicketFlow {
  Tracks  = Ticket.Status;
  Initial = Working;
  event Resolve();
  state Working {
    subscribe Resolve();
    on Resolve { goto Closed; }
  }
  terminal success Closed { }
}

void ResolveTicket(Ticket t) {
  TicketFlow.RaiseResolve(t);
}
```

The same call without naming the workflow, and with an argument the arm routes on:

```osy title="inferred" test app=workflow-raise-inferred
enum ExpenseStatus { Filed, Approved, Rejected }

entity Expense {
  [Required, MaxLength(200)] string Memo;
  ExpenseStatus Status = ExpenseStatus.Filed;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow ExpenseApproval {
  Tracks  = Expense.Status;
  Initial = Filed;
  event Decide(bool approved);
  state Filed {
    subscribe Decide(bool approved);
    on Decide(bool approved) {
      when (approved) { goto Approved; }
      default { goto Rejected; }
    }
  }
  terminal success Approved { }
  terminal error   Rejected { Message = "rejected"; }
}

void DecideExpense(Expense e) {
  Workflow.Raise(e, Decide(true));
}
```

## See also       {#see-also}
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — starting the run this raises on, and the awaited child-workflow form
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the slot that waits for the event, and who may satisfy it
- [Acting on an inbox row (deposit, claim, release)](https://osysharp.com/reference/workflow/inbox-act/) — answering a specific queued slot from an inbox row instead
- [[Authorize] (event)](https://osysharp.com/reference/workflow/authorize/) — the produce-side gate on who may raise an event


---

<!-- https://osysharp.com/reference/workflow/remind/ -->

# Remind (milestone reminders)

> A reminder scheduled off a milestone. Its SCHEDULE is config in the header parens — `After` is the first fire (once, at `enter + After`), and the optional `ThenEvery` repeats it every interval. Its BODY is the block. The bare name `Within` in the schedule reads the enclosing milestone's `Within`, so `After = Within / 2` nudges at the halfway mark.

<!-- id: workflow-remind · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/remind/ -->

## Summary        {#summary}
A **`Remind`** schedules a side-effect off a [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — a nudge that runs while the slot is still waiting,
without transitioning the run. Its **schedule is config**, written in the header parentheses; its **body is the block**.
**`After`** (required) is the first fire, at `enter + After`. **`ThenEvery`** (optional) repeats the body every interval
after that. Reading the header tells you exactly when it fires — there is no hidden anchor. A reminder body may **not**
`goto` — reminders are config, not transitions.

## Signature      {#signature}
```osy syntax
Remind Nudge(After = <TimeSpan>) { <body> }                      // one-shot, at enter + After
Remind Chase(After = <TimeSpan>, ThenEvery = <TimeSpan>) { <body> }  // then repeats every interval
```

`ThenEvery` requires an `After` (it repeats the first fire). The header is config only; the `{ }` block is the body.
The old single-brace form (`Remind { After = …; <statements> }`) is not valid — config and body live in separate places.

**The NAME is required**, and it is the reminder's identity: `Remind Nudge(After = …)`. A milestone may carry more
than one reminder, so an unnamed `Remind(…)` is refused rather than guessed at — the parser says so by name.

## Description    {#description}
A reminder lives inside a milestone (`Assigned { … }` / `Finished { … }`) and shares its ambient `slot`, so its body
can read `slot.Assignee` / `slot.Candidates` and call your own app functions to act on them (e.g. a `Notify(user)` that
sends over a `client`, or a `foreach (var u in slot.Candidates) Notify(u)`). Each firing writes a `Reminded` event to
the timeline (readable via [For(entity).Audit](https://osysharp.com/reference/workflow/audit/)), which is the observable artifact a test asserts on.

**The schedule reads as one timeline:**

- **`After = T`** — the first (and, alone, only) fire, at `enter + T`. `After = Within / 2` reads the enclosing
  milestone's `Within` (the bare name `Within` in a reminder schedule inlines the milestone's `Within` expression) and
  nudges at the halfway mark.
- **`ThenEvery = T`** — repeats the body every `T` after the first fire (`After`, `After + T`, `After + 2T`, …). A poll
  from the start is just `After = T, ThenEvery = T`.

A repeating reminder **stops when its milestone resolves** — Assigned once the slot leaves `Unassigned`, Finished once
it is `Satisfied`, or when the milestone breaches (`Unassigned`/`Unfinished` fires). So the reminder nudges *before* the
deadline while the breach handler owns the deadline itself; they never overlap and a reminder never nags forever.

### A missed cadence is COALESCED, not replayed   {#catch-up}

If the clock passes several due firings before the engine next runs — a scheduler that was down, a long jump in a
test — the reminder fires **once** on the next advance, not once per interval it slept through. A `ThenEvery = 2d`
reminder that goes unattended for a month sends one message, not fifteen.

This is what you want in production: coming back from an outage should not deliver a month of backlog to a customer.
It has one consequence worth knowing, and it is easy to read as a bug in the reminder:

> **A test has to tick the way a scheduler does.** Advancing the clock 30 days and settling once proves that the
> reminder still fires, not that it repeats. To assert a cadence, advance and settle per interval.

```osy syntax
// asserts that it REPEATS — three separate advances, three settles
TestClock.Advance(TimeSpan.FromDays(3));  Workflow.Settle(ticket);   // After = 3d
TestClock.Advance(TimeSpan.FromDays(2));  Workflow.Settle(ticket);   // ThenEvery = 2d
TestClock.Advance(TimeSpan.FromDays(2));  Workflow.Settle(ticket);
```

## Examples       {#examples}
```osy title="nudge the holder before the deadline" test app=workflow-remind
enum Decision   { Approve, Reject }
enum OrderState { Review, Done }

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

entity Order {
  [Required, MaxLength(60)] string Reference;
  [Required] Person Requester;
  OrderState Status;                       // no default: the workflow owns this field
  security { allow read, create, update when IsAuthenticated; }
}

workflow ReminderFlow {
  Tracks    = Order.Status;
  Autostart = true;
  Initial   = Review;

  event Approve(Decision decision);

  state Review {
    subscribe Approve(Decision decision) as Legal {
      Candidates = u => u.Email != "";
      Finished {
        Within = TimeSpan.FromHours(8);
        // The header is config; the { } is the body. A reminder is NAMED — see the signature above.
        Remind Nudge(After = TimeSpan.FromHours(2)) { }
        Unfinished { }
      }
    }
    on Approve(Decision decision) { goto Done; }
  }
  terminal success Done { }
}
```

Nudge the pool halfway through the window, then chase hourly until someone picks it up or the breach fires:

```osy title="nudge, then chase" syntax
Assigned {
  Within = TimeSpan.FromHours(4);
  Remind Nudge(After = Within / 2, ThenEvery = TimeSpan.FromHours(1)) {   // 2h, then 3h, 4h, …
    foreach (var u in slot.Candidates) { Notify(u); }
  }
  Unassigned { … }   // the deadline is owned here, separately
}
```

Drive it in a test and assert on the timeline, not the delivery:

```osy title="a reminder survives — still waiting" syntax
TestClock.Advance(TimeSpan.FromHours(2));              // Remind Nudge(After = Within / 2, …) first fire
Workflow.Settle(po);

var audit = PoApproval.For(po).Audit;
Assert.Equal(1, audit.Count(a => a.Kind == AuditKind.Reminded && a.Slot == "Legal"));
Assert.True(PoApproval.For(po).Legal.IsUnassigned);    // …and we are STILL WAITING
```

## See also       {#see-also}
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — the SLA a reminder is scheduled off (and the source of `Within`)
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — the `Reminded` timeline event each firing writes


---

<!-- https://osysharp.com/reference/workflow/requires/ -->

# Requires — named preconditions, and the live checklist

> Named conditions that must hold before something may happen, declared on a state or on a slot — and readable as a live checklist so a screen can show what is left instead of refusing the button afterwards. Where you declare it decides what it gates: on a slot it gates the deposit into that wait; on a state it IS the state's completion condition.

<!-- id: workflow-requires · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/requires/ -->

## Summary        {#summary}
`Requires` names the conditions standing between an item and progress, and the platform both **enforces** them and
**reports** them. The reporting half is the point: a rule the engine will refuse is a rule a screen should be able to
show before anybody presses anything.

## Signature      {#signature}
```osy title="declaring a named condition and what to tell a human" syntax
Requires {
  <Name> {
    Must    = <predicate over this.Item>;
    Message = "what to tell a human when it does not hold";
  }
  …
}
```

Read the live checklist back:

```osy title="reading the live checklist back for a screen" syntax
<Wf>.For(item).Requirements            // List<RequirementStatus> — the state's AND its slots'
<Wf>.For(item).<Slot>.Requirements     // just that one wait's
```

## Description    {#description}

### Where you declare it decides what it gates    {#scope}
This is the distinction to hold, and the two are not variations of one rule:

| declared on | gates | means |
|---|---|---|
| a **slot** (`subscribe … { Requires { … } }`) | the **deposit** into that wait | *"you may not resolve without a root cause"* |
| a **state** (`state X { Requires { … } }`) | the state's **completion** | *"this state is done when all of these hold"* |

A slot's criteria are checked when somebody tries to fill it — an unmet one refuses the deposit and hands back which
criteria failed, with their messages. A state's criteria replace the default *"every armed slot is satisfied"*
completion test, which is how a quorum is expressed: three voters armed, done at two.

### The checklist returns BOTH, and each row names its slot    {#checklist}
`<Wf>.For(item).Requirements` returns the state's own criteria first, then each of its slots' in declaration order.
Every row carries `Slot` — the wait it gates, or **null** for a state criterion.

That member is not decoration. *"Record a root cause before you can resolve"* and *"triage it before this state is
done"* are different sentences about different acts, and on one flat list a screen cannot group them or say which
button each belongs to.

⚠ **The unscoped read used to return the state's criteria ALONE**, so a slot-scoped `Requires` — the commonest kind —
came back empty. The gate still refused correctly, which made it worse than a plain omission: the app rendered a
checklist saying there was nothing left to do and then refused the button. **An empty checklist and a satisfied one
are the same screen.**

### The rows are LIVE    {#live}
Every predicate is evaluated against `this.Item` at the moment of the read, and nothing is stored. Re-reading after a
change gives the new answer — so a form can re-check as fields are filled.

## Examples       {#examples}
A ticket that must be triaged before the state is done, and cannot be resolved without a root cause:

```osy title="a rule about the wait, and a rule about the state" test app=workflow-requires
enum Stage { Working, Done }

[Principal] entity Agent {
  [Required, MaxLength(80)] string Name;
  security { allow read when IsAuthenticated || IsAnonymous; }
}

entity Ticket {
  [Required, MaxLength(120)] string Subject;
  [MaxLength(200)] string? RootCause;
  bool Triaged;
  Stage State;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow TicketFlow {
  Tracks    = Ticket.State;
  Autostart = true;
  Initial   = Working;

  event Resolve();

  state Working {
    // About the STATE: it is not done until this holds.
    Requires {
      Triaged { Must = this.Item.Triaged; Message = "triage it first"; }
    }

    // About filling THIS WAIT: the deposit is refused until this holds.
    subscribe Resolve() as Resolver {
      Requires {
        RootCause { Must = this.Item.RootCause != null; Message = "record a root cause"; }
      }
    }

    on Resolver { goto Done; }
  }

  terminal success Done { }
}

// What is left to do — everything, grouped by what it blocks.
List<Osysharp.Workflow.RequirementStatus> Outstanding(Ticket ticket) {
  return TicketFlow.For(ticket).Requirements.Where(r => !r.Met).ToList();
}

// Just the wait's own gate — what to show beside the Resolve button.
List<Osysharp.Workflow.RequirementStatus> BeforeResolving(Ticket ticket) {
  return TicketFlow.For(ticket).Resolver.Requirements.ToList();
}
```

## Notes          {#notes}
**`Requires` is not authorization.** It gates the base fact, never the person: *"is the work complete"*, not *"may you
do this"*. Who may fill a wait is the slot's [`Candidates`](https://osysharp.com/reference/workflow/candidates/), checked first — a refusal there is
a `WorkflowAuthorizationException`, while an unmet criterion is a `RequirementsNotMet` carrying the failed criteria.
A UI tells them apart deliberately: one greys a button with a checklist, the other should not have offered it.

**A refused deposit is recorded.** The refusal writes a `Refused` row to the [trail](https://osysharp.com/reference/workflow/audit/), so "why did
this never get resolved" has an answer.

**`complete when` is the other way to say a state is done.** [`complete when (<predicate>)
goto <State>`](https://osysharp.com/reference/workflow/complete-when/) states one condition and where it goes; a state-level `Requires` states several NAMED ones with
messages, and is what you want when a human needs to be told which is missing.

## See also       {#see-also}
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the slot a `Requires` is declared inside
- [Transitions — where this item may go next](https://osysharp.com/reference/workflow/transitions/) — where the item may go next, the read this checklist sits beside
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — who may fill the wait, the gate checked before this one
- [Acting on an inbox row (deposit, claim, release)](https://osysharp.com/reference/workflow/inbox-act/) — depositing, which is what a slot's criteria gate


---

<!-- https://osysharp.com/reference/workflow/service-hours/ -->

# ServiceHours (SLA-accrual windows)

> A schedule the SLA clock accrues within — the platform WALKS its weekly windows (and holiday exceptions) to advance ticks and compute deadlines. The platform defines the shape; the app fills the rows (BusinessHours, AroundTheClock) and assigns which one governs an instance. No windows ⇒ the identity schedule (ticks == wall-clock).

<!-- id: workflow-service-hours · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/service-hours/ -->

## Summary        {#summary}
**`ServiceHours`** is the window an SLA clock accrues within. The platform's tick-accrual engine walks a schedule's
weekly `Windows` (and any holiday `Exceptions`) to advance SLA time and compute deadlines — so a ticket on a
business-hours schedule simply doesn't burn its clock at night, while one on a 24/7 schedule accrues around the clock.
The **shape is platform** (the engine must walk it), the **rows are the app's** (it seeds `BusinessHours` /
`AroundTheClock`, assigns which one governs an instance, and owns everything about the SLA's meaning).

## Signature      {#signature}
```osy
public entity ServiceHours {
  string Name;
  Zone   Zone;                    // REQUIRED — the zone the windows' local times are read in (DST-correct)
  [ForeignKey(ServiceHours)] ServiceWindow[]    Windows;     // recurring weekly open windows — Mon 09:00–17:00, …
  [ForeignKey(ServiceHours)] ServiceException[] Exceptions;  // holiday / special-hours date ranges
}

public entity ServiceWindow {
  ServiceHours ServiceHours;
  DayOfWeek Day;                  // the C# built-in enum (Sunday = 0 … Saturday = 6)
  TimeSpan  Start;  TimeSpan End; // local time-of-day, as a TimeSpan from midnight
}

public entity ServiceException {
  ServiceHours ServiceHours;
  DateTime From;  DateTime To;
  bool     IsClosed;              // the clock does not accrue across From..To
  string   Reason;
}
```

## Description    {#description}
`ServiceHours` is **ordinary app data** (a data-DB entity), editable through the app's own UI — not a declaration
block. An app seeds the schedules it needs and points an instance at one (typically snapshotted in the workflow's
`Start { }` from a contract/severity matrix). The platform reads the rows and walks them; it never defines them.

- **`Windows`** are recurring weekly intervals in local time. The parent's `Zone` gives them a DST-correct instant
  (a `09:00` Stockholm window is a different UTC instant in summer and winter). A schedule with **no windows** is the
  **identity schedule** — ticks equal wall-clock — which is the 24/7 / "AroundTheClock" case.
- **`Zone` is required.** An SLA schedule must always state the zone its hours are measured in, so a row can never be
  saved ambiguous — the platform rejects it at **commit** (UI edit or seed), not just at compile. A 24/7 schedule names
  a zone too (`Zone = "UTC"`); it's inert there (with no windows the walk never reads it) but keeps every schedule
  self-documenting.
- **`Exceptions`** are date-range overrides applied on top of the windows. An `IsClosed` exception (a public holiday)
  removes accrual across its `From..To`; the `Reason` is for humans.
- **`DayOfWeek`** is the C# built-in enum, so `w.Day == DayOfWeek.Monday` reads and stores exactly as in C#.

**Binding it to a workflow.** A workflow names the schedule its clocks accrue within with a `ServiceHours = <expr>;`
setting (a value over `this.Item` resolving to a `ServiceHours` ref, typically snapshotted onto the entity in `Start`).
Every SLA clock on the run — a state's `Expire`, a milestone's `Within`, a reminder — then advances only inside that
schedule's windows, so a deadline computed from a 4-hour budget lands after the intervening nights and weekends, not 4
wall-clock hours later. With no `ServiceHours` binding a run uses the identity schedule (24/7).

```osy title="binding a schedule to a workflow" syntax
workflow SupportTicket {
  ServiceHours = this.Item.ServiceHours;   // the schedule this run's clocks walk
  Accrues      = [Open, Working];          // and the states in which they run at all
  ...
}
```

⛔ **The binding LOOKS UP a schedule; it must not create one.** `ServiceHours = <expr>;` is a **setting**, and a
setting is re-evaluated on every clock arm, every state entry and **every inbox read**. On a read path the engine's
work is never committed, so a create-if-missing bound here writes rows that are thrown away and hands back a row that
does not exist — leaving the app with an **empty inbox and no error**. The compiler refuses it, naming the function
that writes; seed the schedule once, from a function you call yourself, and let the setting only look it up. The same
rule covers every expression-valued workflow setting (`Autostart`, `Deadline`, `Expire`, `Within`, `Backoff`, `When`,
`Assignee`, `Reassign`, `CompleteWhen`, a route arm's guard) — reading and calling are fine there; only writing is
refused.

```osy title="seed once; the setting only looks it up" syntax
void SeedSchedules() {                                   // called from the app's own setup, ONCE
  if (ServiceHours.Any(h => h.Name == "Business hours")) { return; }
  var hours = new ServiceHours { Name = "Business hours", Zone = Zone.Of("Europe/Stockholm") };
  new ServiceWindow { ServiceHours = hours, Day = DayOfWeek.Monday, Start = TimeSpan.FromHours(9), End = TimeSpan.FromHours(17) };
  UnitOfWork.Commit();
}

ServiceHours BusinessHours() { return ServiceHours.Single(h => h.Name == "Business hours"); }

workflow SupportTicket {
  ServiceHours = BusinessHours();          // ✓ a lookup
// ServiceHours = SeedAndReturnHours();    // ✗ refused — a setting that writes
  ...
}
```

### One schedule per tenant   {#per-tenant}
`ServiceHours` carries no tenant column, and a `partial entity ServiceHours { Organization? Org; }` is refused — a
partial states security, it does not reshape a platform table. **The reference goes the other way round**, on your own
tenant entity, which you do own and can shape freely. Seed one schedule per tenant, point each tenant's row at its
own, and read it through the run:

```osy title="two organisations, two schedules, two zones" syntax
entity Organization {
  [Required, MaxLength(80)] string Name;
  ServiceHours? Hours;                       // ← the FK lives on YOUR entity
  security { allow read, create, update when IsAuthenticated; }
}

workflow SupportTicket {
  ServiceHours = this.Item.Org.Hours;        // ← each run walks its own tenant's calendar
  ...
}
```

Every run then accrues in its own tenant's windows and zone — a Stockholm customer's four-hour SLA and a New York
customer's land at different instants, from one workflow declaration.

**Two schedule-shaped things, deliberately split.** `ServiceHours` is *when the SLA clock accrues* — the customer's
promise window. It is **not** availability (who is on shift): rosters, leave, and follow-the-sun live entirely in the
app and the platform never consults them. The promise (ServiceHours) and the people (availability) are different
concerns with different owners.

## Examples       {#examples}
```osy title="a schedule the engine's clocks accrue against" test app=workflow-service-hours
enum OrderState { Open, Done }

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

entity Order {
  [Required, MaxLength(60)] string Reference;
  OrderState Status;                       // no default: the workflow owns this field
  security { allow read, create, update when IsAuthenticated; }
}

// The schedule entities ship with the platform; an app that reaches them says who may.
partial entity ServiceHours   { security { allow read, create when IsAuthenticated; } }
partial entity ServiceWindow  { security { allow read, create when IsAuthenticated; } }

// Two schedules: one that accrues around the clock, one that only counts business hours.
void SeedSchedules() {
  new ServiceHours { Name = "AroundTheClock", Zone = Zone.Of("UTC") };          // no windows ⇒ 24/7

  var biz = new ServiceHours { Name = "BusinessHours", Zone = Zone.Of("Europe/Stockholm") };
  new ServiceWindow { ServiceHours = biz, Day = DayOfWeek.Monday,
                      Start = TimeSpan.FromHours(9), End = TimeSpan.FromHours(17) };
}
```

Seed a 24/7 schedule and a Mon–Fri business-hours one with a holiday:

```osy title="seed two schedules" syntax
var aroundTheClock = new ServiceHours { Name = "AroundTheClock", Zone = "UTC" };   // no windows ⇒ accrues 24/7
var bizHours = new ServiceHours { Name = "BusinessHours", Zone = "Europe/Stockholm" };
foreach (var d in [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday]) {
  new ServiceWindow { ServiceHours = bizHours, Day = d, Start = TimeSpan.FromHours(9), End = TimeSpan.FromHours(17) };
}
new ServiceException { ServiceHours = bizHours, From = new DateTime(2026, 6, 19), To = new DateTime(2026, 6, 19),
                       IsClosed = true, Reason = "Midsommarafton" };
```

## See also       {#see-also}
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — the SLA clock whose ticks accrue within these windows
- <span class="planned" title="this page is planned and not written yet">workflow-state</span> — `Accrues` names the states in which the clock runs
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the slot whose promise the clock measures


---

<!-- https://osysharp.com/reference/workflow/park-label/ -->

# Step labels (naming a child run so it survives a new version)

> Every child workflow you AWAIT carries a label — a literal string you choose, naming that step. Awaiting parks the run, sometimes for days, which is long enough for a new version of the app to be deployed underneath it; the label is what lets the new version recognise the step the run is sitting at and the work it has already finished. Two steps in one workflow may not share one. A fire-and-forget start never parks, so it needs none.

<!-- id: workflow-park-label · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/park-label/ -->

## Summary        {#summary}
`await saga.Run("ship", shipment)` and `await Workflow.Run("fulfil", order)` **park** the run: it stops, durably,
until the child finishes. A three-step approval saga can sit parked for a week — and a week is long enough for the app
to be deployed again.

When that happens, the new version has to answer one question about the parked run: **where is it, and what has it
already done?** "Step 2 of 5" is only an answer if both versions agree on what "step 2" *is*. The **label** is that
agreement, and it is why every step carries one.

## Signature      {#signature}
```osy syntax
await Workflow.Run("fulfil", order);                        // label, then the entity
Workflow.Run(order);                                        // fire-and-forget — never parks, so no label
await saga.Run("reserve", hotel);                           // label, then the step
await saga.Run("reserve", hotel, () => Cancel(hotel));      // …and its compensation, as usual
```

The label is a **literal string**. It cannot be a variable or an expression: its whole job is to be the *same* string
in a later version of the body, and a value computed while the run executes could differ between the two readings that
have to match.

## Description    {#description}

### Why it is required rather than inferred   {#required}
The obvious convenience is to derive a label when you leave it out — from the child workflow's name, or from the step
variable. Both are rejected, and the reason is worth stating plainly: **a derived name is not one you chose.** Rename
the child workflow, or rename a local from `hotel` to `outbound`, and the derived label changes — silently, in a way
that has nothing to do with the rename, and that breaks the migration of every run currently parked at that step.

The cost of the alternative is one string literal per step. You are not writing thousands of workflows an hour, and
what the literal buys is that **every step is migratable by construction** — there is no such thing as a run parked
somewhere a new version cannot find.

### Two steps may not share a label   {#unique}
A label is an identity, and two things under one identity is not an identity:

```osy syntax
await saga.Run("leg", outbound);
await saga.Run("leg", inbound);     // compile error
```

> workflow 'BookingSaga': two steps here are both labelled "leg", so a resumed run could not tell which of them it had
> already finished. Give them different labels.

Uniqueness is checked across the **whole workflow** — its start body, every state's `enter` body, and every route —
because a run can be parked at any of them.

### Why that error is at compile time   {#compile-time}
This explains why you are asked now rather than never.

Two indistinguishable steps are only a *problem* when a parked run meets a new version — at **deploy** time, possibly
weeks later. Reporting it then would be useless: the run parked with the duplicate already in place, so relabelling
afterwards cannot help **that** run. It would be a complaint nobody could act on, repeating on every deploy for as long
as the run lived.

Asked at compile time it is the opposite: you have not deployed, no run exists, and typing two names fixes it
permanently.

## Examples       {#examples}

A booking saga with **two legs of the same kind** — an outbound flight and a return. Both run `FlightFlow`, so the
labels are the only thing distinguishing them, and they are what let a run parked on the return leg still be
recognised as "outbound done, return in progress" after a redeploy.

```osy title="the world the saga runs in" test app=wf-park-label
enum BookStatus { Start, Booked, Failed }
enum LegStatus  { Waiting, Done, Bad }

entity Booking {
  [Required, MaxLength(20)] string Ref;
  BookStatus Status = BookStatus.Start;
}

entity FlightLeg {
  [Required] Booking Booking;
  [Required, MaxLength(10)] string Direction;
  LegStatus Status = LegStatus.Waiting;
  bool Cancelled;
}

// The compensation. `Status` belongs to FlightFlow (it is what the workflow `Tracks`), so app code cannot assign it —
// a compensation records its own outcome on a field it owns.
void CancelFlight(FlightLeg leg) { leg.Cancelled = true; }

workflow FlightFlow {
  Tracks = FlightLeg.Status; Autostart = false; Initial = Waiting;
  event Finish();
  state Waiting { subscribe Finish(); on Finish { goto Done; } }
  terminal success Done { }
  terminal error   Bad  { Message = "the leg failed"; }
}
```

```osy title="two legs of one kind, told apart by their labels" test app=wf-park-label
workflow BookingSaga {
  Tracks = Booking.Status; Autostart = false; Initial = Start;
  state Start {
    enter {
      var saga = Workflow.BeginSaga();
      try {
        var outbound = Workflow.Once("make-outbound", () => new FlightLeg { Booking = this.Item, Direction = "out" });
        await saga.Run("outbound-flight", outbound, () => CancelFlight(outbound));

        var inbound = Workflow.Once("make-inbound", () => new FlightLeg { Booking = this.Item, Direction = "back" });
        await saga.Run("return-flight", inbound, () => CancelFlight(inbound));

        saga.Complete();
        goto Booked;
      }
      catch (WorkflowError) { goto Failed; }
      finally { await saga.DisposeUnwind(); }
    }
  }
  terminal success Booked { }
  terminal error   Failed { Message = "the booking failed"; }
}
```

## Notes          {#notes}
- A label names a **step in the body**, not a child run. Two different runs of the same workflow each have their own
  step at that label; the label distinguishes *places in the code*, not instances.
- Labels are compared **exactly**, case included.
- **A fire-and-forget `Workflow.Run(order)` — one written without `await` — takes no label.** Awaiting is what makes
  a start a park point, and only a park point has an identity a later version must match; a start you do not wait on
  has nothing to re-find. `saga.Run` always takes one, because every leg is joined.
- The same reasoning, for a different mechanism, gives [Workflow.Once (run a step at most once)](https://osysharp.com/reference/workflow/once/) its step label. A `Workflow.Once` label keeps
  an *idempotency key* stable across a deploy; a step label keeps a *position* recognisable across one.

## See also       {#see-also}
- [Workflows that outlive the code that started them](https://osysharp.com/reference/workflow/change-over-time/) — why a park point needs a name at all: what a deploy does to a run sitting at one
- [Workflow.BeginSaga (a compensating saga scope)](https://osysharp.com/reference/workflow/beginsaga/) — the saga scope these steps run in
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — starting a child workflow, awaited or not
- [Workflow.Once (run a step at most once)](https://osysharp.com/reference/workflow/once/) — the other label, and the other kind of durability
- [Migrating runs that are still in flight](https://osysharp.com/reference/workflow/migration/) — what a deploy does to runs that are already parked


---

<!-- https://osysharp.com/reference/workflow/tracks/ -->

# Tracks and Initial (the field a workflow drives)

> Names the enum field a workflow owns and the state a run starts in. No application code may write that field, and when the workflow autostarts it also supplies the field's starting value, so the entity declares no default.

<!-- id: workflow-tracks · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/tracks/ -->

## Summary        {#summary}
A workflow drives one field on one entity: the field that says where a row has got to. **`Tracks`** names that field
and **`Initial`** names the state a fresh run begins in.

Together they hand the field over. From that point it is the workflow's, not the application's — it moves only by
transition, and reading it anywhere tells you the truth about the run. When the workflow starts with the row, it
supplies the field's opening value too.

## Signature      {#signature}
```osy syntax
workflow <Name> {
  Tracks  = <Entity>.<Property>;
  Initial = <State>;
  …
}
```
The property is an enum-typed member of the tracked entity, and every state and terminal in the workflow must be a
member of that enum — so the field's type *is* the workflow's vocabulary of states.

## Description    {#description}
### When the workflow starts with the row, the field needs no default   {#autostart}
An `Autostart = true` workflow begins the moment its row exists, so `Initial` *is* the field's value from the start
and the entity does not restate it:

```osy title="the field is declared bare — the workflow supplies its starting value" test app=workflow-tracks
enum ApprovalStage { Pending, Approved, Rejected }

entity Invoice {
  [Required] [MaxLength(120)] string Description;
  [Required] decimal Amount;
  ApprovalStage Stage;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

workflow ExpenseApproval {
  Tracks    = Invoice.Stage;
  Autostart = true;
  Initial   = Pending;

  event Decide(bool approved);

  state Pending {
    subscribe Decide(bool approved);
    on Decide(bool approved) {
      when (approved) { goto Approved; }
      default { goto Rejected; }
    }
  }

  terminal success Approved { }
  terminal error   Rejected { Message = "rejected"; }
}
```

A new `Invoice` reads back `Pending` with nothing in the source saying so twice.

This matters more than it looks. Elsewhere in the language a bare enum member is **required** — the platform will not
invent a value for it, because the first member of an enum is a position, not a decision, and reordering the members
would silently change what every new row means. A tracked field is the one case where a real value is genuinely
available: the workflow declared it.

### WHEN does it start, exactly?   {#autostart-timing}
"Begins the moment its row exists" is precise, not a loose way of saying "soon": an `Autostart = true` workflow's
initial `enter{}` runs **synchronously, inside the same commit** that created the row — before the next statement
after `UnitOfWork.Commit()` executes, in the SAME function, with no pump or wait of any kind. A read immediately
after that commit already sees whatever the `enter{}` body wrote:

```osy title="the enter body has already run by the time the NEXT function looks" run app=workflow-tracks-autostart-timing
enum TicketState { Open, Closed }

entity Ticket {
  [MaxLength(80)] string Title;
  TicketState Status;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

entity AuditEntry {
  [Required, MaxLength(80)] string Note;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

workflow TicketFlow {
  Tracks = Ticket.Status;
  Autostart = true;
  Initial = Open;
  event Close();
  state Open {
    enter { new AuditEntry { Note = "opened" }; }
    on Close { goto Closed; }
  }
  terminal success Closed { }
}

[Test]
void AutostartedEnterBody_RunsInTheSameCommit_NoPumpNeeded() {
  new Ticket { Title = "t" };
  UnitOfWork.Commit();

  // No `Workflow.Settle(...)` here — the enter{} body has already run, in the commit above.
  Assert.Equal(1, AuditEntry.Count());
}
```

⚠ **This is a deliberate platform guarantee, not an implementation detail that happens to hold today.** In
production, autostart is ALSO delivered by a durable `wf-start` job the platform enqueues at commit — the backstop
for a row committed by a path that is not Osy# (a REST write, an import) and for recovering a start this process
died during. But an Osy# `UnitOfWork.Commit()` drives the autostart pass itself before returning, specifically so
"create a row, then ask about it" reads the way it looks. Starting the same run twice is a no-op by design, so the
durable job racing (or duplicating) the in-process start is harmless.

**`Workflow.Settle(entity)` is a TEST-ONLY primitive** for a different problem: draining DUE, ALREADY-SCHEDULED work
— timers, reminders, a milestone's deadline — deterministically, without a real clock to wait on. It has no
counterpart in production code, where that same work is drained by the durable dispatcher on its own schedule. Reach
for it in a test that needs to observe a timer fire or a deadline expire; an autostarted `enter{}` needs no `Settle`
at all, because it has already run by the time `UnitOfWork.Commit()` returns.

**A caller with no Osy# session can still trigger an autostart** — a webhook, a scheduled tick, a REST write — just
on the durable job's timing rather than in the same instant: the row lands, the write's own commit finishes (with no
run attached yet), and the durable `wf-start` job picks it up and starts the run shortly after. What is guaranteed
synchronous is specifically an Osy# `UnitOfWork.Commit()`; every other write path gets the same eventual guarantee,
on the dispatcher's cadence rather than in-line.

### Restating it is refused   {#no-restate}
Writing the default anyway is a compile error, not a redundancy the compiler tolerates:

```osy title="refused — the workflow already supplies it" syntax
ApprovalStage Stage = ApprovalStage.Pending;   // 'Stage' on 'Invoice' is owned by workflow 'ExpenseApproval' …
```

The reason is drift. Two copies of one fact stay in step only while someone keeps them there, and the copy a reader
believes is the one in the entity — so the day `Initial` changes and the declaration does not, the source says
something false and nothing reports it. One statement of the fact cannot disagree with itself.

### The field is read-only to application code   {#read-only}
No function, action, or object initializer may assign a tracked field; a workflow's own handlers may. This is the
same ownership seen from the other side — if application code could set the field, the state would no longer mean
"where this run has got to", it would mean "whatever was written last".

```osy title="the state moves by transition, never by assignment" syntax
invoice.Stage = ApprovalStage.Approved;   // refused — the workflow drives it
```

To move a run, send it an event and let a route transition it.

### When the run starts later, the field's earlier value is yours to state   {#late-start}
`Initial` is where the **run** begins, not where the **row** begins. Those are the same moment only when the workflow
autostarts. If it does not — `Autostart = false`, or a condition that is not yet true — the row exists for a while
with no run attached, and what the field reads in that window is a fact only you know:

```osy title="a report is a draft before it is anyone else's problem" test app=workflow-tracks-draft
enum ReportStage { Draft, InApproval, Approved, Rejected }

entity Report {
  [Required] [MaxLength(120)] string Title;
  DateTime? SubmittedAt;
  ReportStage Stage = ReportStage.Draft;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

workflow ReportApproval {
  Tracks    = Report.Stage;
  Autostart = this.Item.SubmittedAt != null;   // starts on submit, not on create
  Initial   = InApproval;

  event Decide(bool approved);

  state InApproval {
    subscribe Decide(bool approved);
    on Decide(bool approved) {
      when (approved) { goto Approved; }
      default { goto Rejected; }
    }
  }

  terminal success Approved { }
  terminal error   Rejected { Message = "rejected"; }
}
```

`Draft` is a member of the tracked enum but not a state the workflow ever occupies — it is where a report sits before
the workflow has anything to do. Here the default is required, and it is not refused: the workflow is not claiming to
supply it.

**So the question "does my tracked field need a default?" has one answer: does the workflow start when the row is
created?** If yes, the workflow supplies the value and you must not restate it. If no, it is yours.

## Examples       {#examples}
An `Initial` that is decided per row rather than fixed — the field then starts wherever the expression lands when the
run begins, and a declared default is legal for the same reason as above:

```osy title="a starting state that depends on the row" syntax
workflow ExpenseApproval {
  Tracks  = Invoice.Stage;
  Initial = this.Item.Amount > 500 ? ApprovalStage.Pending : ApprovalStage.Approved;
  …
}
```

## Notes          {#notes}
**Every state must be a member of the tracked enum.** A state the enum does not name is a compile error — the two
declarations are one vocabulary, so they cannot drift apart either.

**`Autostart` decides when a run begins** — and therefore whether the field's opening value is the workflow's to
supply or yours to declare. That is the one thing to carry away from this page.

## See also       {#see-also}
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — declaring what a state waits for
- [complete when (a state's own completion condition)](https://osysharp.com/reference/workflow/complete-when/) — leaving a state on a condition rather than an event
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — starting child work from a state


---

<!-- https://osysharp.com/reference/workflow/transitions/ -->

# Transitions — where this item may go next

> One row per move this instance can make right now, with each arm's guard evaluated against it. A board offers only the lanes a card may actually reach instead of accepting a drop and having the engine refuse it afterwards — and `Workflow.Raise(item, move)` takes one of them by handing the row back.

<!-- id: workflow-transitions · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/transitions/ -->

## Summary        {#summary}
**`<Wf>.For(item).Transitions`** is the answer to *"where may this item go?"*, asked of one running instance and
answered **now** — every guard re-evaluated against this item's current data.

It is a **reflection** of the graph the engine enforces and never a substitute for it. The deposit path re-checks
everything, so a stale or spoofed read buys nothing; the value is that a screen can show the right buttons instead of
finding out on the click.

## Signature      {#signature}
```osy syntax
<Wf>.For(item).Transitions      // List<TransitionView>
Workflow.Raise(item, move)      // take one — `move` is a row this read returned
```

## Description    {#description}

### The row   {#row}
| member | what it is |
|---|---|
| `Event` | the event that fires this move — what you would deposit |
| `Target` | the state it lands in, as an **identifier** — what code compares against. The **current** state when the arm has no `goto` (it runs a body and waits again) |
| `TargetLabel` | the same state as **words**: the tracked enum member's `[Label]`, or its name when it declares none |
| `Allowed` | is it open **right now** — this arm's guard against this item |
| `Reason` | why it is closed, for a human; null when open |
| `NeedsInput` | does the event take arguments — a drop gesture cannot supply them, so a surface must open a form |
| `Slot` | the **wait** this move fills, or null when nothing is waiting for it |

### Closed moves are RETURNED, not hidden    {#closed-moves}
An arm whose guard does not hold comes back with `Allowed = false` and a `Reason`. A target the user cannot reach is
worth **showing as unavailable** rather than omitting: *"you may not move it there yet"* is a different message from a
board that silently has fewer lanes than the workflow does.

### `Slot` — a wait you can fill, or a command you can issue    {#slot}
These are different gestures and they need different affordances.

A move **with** a `Slot` answers a wait: somebody may hold it, it appears on a [board](https://osysharp.com/reference/workflow/work-by-item/), it has
a `Candidates` gate and possibly an SLA. A move with **no** `Slot` is a command — most often one the workflow declares
once, live in every non-terminal state:

```osy syntax
workflow TicketFlow {
  on Cancel { goto Cancelled; }        // no slot waits for this; it is a menu item, not a lane
  …
}
```

Both are real moves and both appear here. Without `Slot` they arrive as indistinguishable rows and a page has to
re-derive the workflow's shape to know which is which.

### `Target` is an identifier; `TargetLabel` is the words    {#target-label}
A state's name is the tracked enum's **member** name, so the member's `[Label]` is the label every other surface
already renders for that value — a board's lanes, a card, a form. `TargetLabel` brings it here.

```osy syntax
enum TicketStatus {
  Open,
  [Label("Awaiting customer")] AwaitingCustomer,
}
```

Both fields exist because both are wanted, and they are not interchangeable: a screen **shows** `TargetLabel` and
**compares against** `Target`. Rendering the identifier puts a machine spelling in front of a person; comparing
against the label breaks the moment somebody adds a `[Label]`.

⚠ **`Event` has no counterpart, and that is a statement rather than a gap.** An event is a declared name and carries
no `[Label]`, so there is nothing to fall back from. An app rendering `m.Event` directly is showing an identifier;
labelling its own verbs is currently the only answer.

### One row per EVENT, not per arm    {#folding}
An event may declare several arms (`on X { when (a) { goto A; } default { goto B; } }`) and the engine fires the first
whose guard holds. The read folds them the same way, so `Target` is the arm that **would actually be taken** right
now — which is the question a caller is asking.

The same fold gives **nearest scope wins**: a state's own arm is considered before a workflow-level one for the same
event, exactly as dispatch does.

### What is NOT a move    {#not-moves}
`Complete`, `Expire` and `Deadline` arms are engine-fired — a timer, or a requirement becoming satisfied. Nobody drops
a card to make a deadline pass, so offering them would describe a UI that cannot exist. A run that is not `Waiting`
(terminal, failed, cancelled) returns an **empty list**, which lets a board render *"no moves"* honestly.

### How do I take one of the listed moves?    {#taking}
`Workflow.Raise(item, move)` hands the **row** back rather than naming an event. Every other raise form names its
event at compile time; this list is a runtime one, so passing the view is what keeps it honest — you can only take a
move the engine itself listed for this instance.

## Examples       {#examples}
A card wall: the lanes this card may be dropped into, and the commands beside them.

```osy title="the moves a card can make, and taking one" test app=workflow-transitions
enum CardStatus { Triage, Doing, Done, Cancelled }

[Principal] entity Person {
  [Required, MaxLength(80)] string Name;
  security { allow read when IsAuthenticated || IsAnonymous; }
}

entity Card {
  [Required, MaxLength(200)] string Title;
  CardStatus Status;
  bool Signed;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow CardFlow {
  Tracks    = Card.Status;
  Autostart = true;
  Initial   = Triage;

  event Start();
  event Finish(string note);
  event Cancel();

  // Declared on the WORKFLOW: available in every non-terminal state, and no slot waits for it.
  on Cancel { goto Cancelled; }

  state Triage {
    subscribe Start();
    on Start { goto Doing; }
  }

  state Doing {
    subscribe Finish(string note);
    on Finish { when (this.Item.Signed) { goto Done; } }
  }

  terminal success Done { }
  terminal error   Cancelled { Message = "cancelled"; }
}

// What the buttons say. `TargetLabel` is for the person; `Target` is what the page compares against.
List<Osysharp.Workflow.TransitionView> Choices(Card card) {
  return CardFlow.For(card).Transitions.Where(t => t.Allowed).ToList();
}

// The lanes: moves that fill a wait, which is what a board drops into.
List<Osysharp.Workflow.TransitionView> Lanes(Card card) {
  return CardFlow.For(card).Transitions.Where(t => t.Slot != null).ToList();
}

// The menu: moves with nothing waiting for them.
List<Osysharp.Workflow.TransitionView> Commands(Card card) {
  return CardFlow.For(card).Transitions.Where(t => t.Slot == null).ToList();
}

// Taking one, by handing the row back.
void TakeFirstOpen(Card card) {
  var move = CardFlow.For(card).Transitions.Where(t => t.Allowed && !t.NeedsInput).First();
  Workflow.Raise(card, move);
}
```

## Notes          {#notes}
**It is re-read, never stored.** Guards are evaluated at the moment of the call, so a page that wants live buttons
re-reads rather than caching — the same relationship a `canPress` policy has with the server that goes on enforcing it.

**A `live` read over this wakes on the ITEM.** A workflow read subscribes to the entity the run is FOR, never to its
own row type — those rows are synthesised per call and nobody commits one. So a raise wakes it (the tracked property
moves), and so do [`Claim`](https://osysharp.com/reference/workflow/inbox-act/) / `Release` / [`Assign`](https://osysharp.com/reference/workflow/assign/), which change no property
on the item but signal it deliberately. Without that a hand-over left the buttons beside it answering from before.

**`Allowed` is about the ARM's guard, not about you.** Whether *this caller* may fill a particular wait is the slot's
own [`Candidates`](https://osysharp.com/reference/workflow/candidates/), asked with `<Wf>.For(item).<Slot>.Candidates(u)`.

## See also       {#see-also}
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — whether the viewer may fill a wait, which `Allowed` does not answer
- [Assign — handing a slot to a named colleague](https://osysharp.com/reference/workflow/assign/) — handing a wait to a named colleague once you know which slot it is
- [Requires — named preconditions, and the live checklist](https://osysharp.com/reference/workflow/requires/) — what must hold before a move can be taken, as a live checklist
- [Workflow.WorkByItem&lt;T&gt; (one row per item — the board read)](https://osysharp.com/reference/workflow/work-by-item/) — the board this read draws the lanes for
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — raising an event by name, when the author knows it at compile time


---

<!-- https://osysharp.com/reference/workflow/wall-time-clock/ -->

# Wall-time clocks (Accrues = false)

> A timer that measures REAL time instead of SLA time. Written as a block on any clock-declaring setting — `Expire`, `Deadline`, a milestone's `Within`, a `Remind` cadence — `Accrues = false` exempts that one clock from BOTH SLA gates: the run's service-hours schedule and the per-state accrual pause. It is what lets an auto-close fire in a state that carries no service promise.

<!-- id: workflow-wall-time-clock · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/wall-time-clock/ -->

## Summary        {#summary}
A workflow clock measures **SLA time** by default: it advances only inside the run's `ServiceHours` windows, and only
while the run sits in a state named by `Accrues`. That is right for a promise ("first response within 4 hours") and
wrong for an operational timer ("close it 7 days after it was resolved") — seven days there means seven **real** days,
and the state a resolved ticket sits in accrues no promise at all.

`Accrues = false` says which kind a clock is, in the declaration itself.

## Signature      {#signature}
```osy syntax
// the scalar form is unchanged and means SLA ticks
Expire = TimeSpan.FromHours(4);

// the block form states the gate
Expire   { Within = TimeSpan.FromDays(7);  Accrues = false; }
Deadline { Within = TimeSpan.FromDays(30); Accrues = false; }

Assigned {
  Within  = TimeSpan.FromHours(2);
  Accrues = false;                                    // a real-time nudge, not a service promise
  Remind Ping(After = TimeSpan.FromMinutes(10), ThenEvery = TimeSpan.FromMinutes(10), Accrues = false) { … }
}
```

## Description    {#description}
`Accrues = false` exempts one clock from **both** SLA gates:

- **The service-hours schedule.** A wall-time clock walks real time, so a 7-day budget lands 7×24 hours later
  regardless of nights, weekends or holidays. Re-pointing the run at a different schedule mid-flight does not
  retroactively change what its remaining budget means.
- **The per-state accrual pause.** A state outside the workflow's `Accrues` list pauses every SLA clock on the run.
  A wall-time clock keeps running through it.

That second exemption is the point. **An ordinary `Expire` in a state outside `Accrues` cannot fire at all** — the
accrual pause runs on the very state-entry that armed the timer, so the clock is suspended before it is ever swept.
The compiler refuses that shape and names the two ways out:

```text
state 'Resolved' declares an `Expire` but is not in this workflow's `Accrues` list, so its SLA clock is paused the
moment the run enters the state and the timer can never fire. If this is a wall-time timer (an auto-close, a cadence),
say so: `Expire { Within = <time>; Accrues = false; }`. If it is an SLA promise, add the state to `Accrues`.
```

**It is declared, never inferred.** A state moving in or out of `Accrues` is a change to what the run *promises*; it
must not also, silently, change what an existing timer *measures*. So the gate is written on the clock, and a clock
that says nothing is an SLA clock — the reading it has always had.

**Every clock-declaring setting takes it**, and they mean the same thing everywhere: the workflow-level `Expire` and
`Deadline`, a per-state `Expire`, a milestone's `Within`, and a `Remind` cadence. A per-state `Expire` overrides the
workflow-level one **together with its gate** — the flag travels with whichever budget won, so an inherited default
never picks up a state's flag.

## Examples       {#examples}
One support flow carrying both kinds of clock. `Working` accrues the promise, so its `Expire` and its milestone
`Within` are SLA time. `Resolved` accrues nothing — the promise is already met — yet still auto-closes after seven
**real** days, and the nudge cadence is in real minutes even though the milestone it hangs off is a service promise:

```osy title="SLA clocks and wall-time clocks side by side" test app=wf-wall-time-clock
enum TicketStatus { Working, Resolved, Closed, Escalated }

entity Ticket {
  [Required, MaxLength(200)] string Title;
  TicketStatus Status = TicketStatus.Working;
}

workflow SupportTicket {
  Tracks    = Ticket.Status;
  Autostart = false;
  Initial   = Working;
  Accrues   = [Working];                   // Resolved is deliberately NOT here

  event Resolve();
  event Review();

  state Working {
    Expire = TimeSpan.FromHours(4);        // an SLA clock: service hours, paused whenever the run parks
    on Expire { goto Escalated; }

    subscribe Review() as Reviewer {
      Assigned {
        Within = TimeSpan.FromHours(4);    // 4 SERVICE hours to pick it up…
        Remind Ping(After = TimeSpan.FromMinutes(10), ThenEvery = TimeSpan.FromMinutes(10), Accrues = false) {
          Log.Information("still unassigned");   // …nudged every 10 REAL minutes
        }
        Unassigned { goto Escalated; }
      }
    }

    subscribe Resolve();
    on Resolve { goto Resolved; }
  }

  state Resolved {
    Expire { Within = TimeSpan.FromDays(7); Accrues = false; }   // seven REAL days
    on Expire { goto Closed; }
  }

  terminal success Closed    { }
  terminal error   Escalated { Message = "escalated"; }
}
```

Drop the `Accrues = false` from `Resolved` and the app stops compiling — that timer could never have fired.

## See also       {#see-also}
- [ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/) — the schedule an SLA clock accrues within, and which a wall-time clock ignores
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — `Within`, the SLA a slot must be reached inside
- [Remind (milestone reminders)](https://osysharp.com/reference/workflow/remind/) — the reminder whose cadence this most often applies to


---

<!-- https://osysharp.com/reference/workflow/body-security/ -->

# What a workflow body may write

> A workflow body runs on a system data context, so its writes are NOT gated by the entity's `security {}` block. The person who raised the event is authorized to RAISE it — `[Authorize]` and a slot's `Candidates` decide that — and everything the transition then writes lands regardless of what that person could have written themselves.

<!-- id: workflow-body-security · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/body-security/ -->

## Summary        {#summary}
A workflow body writes as the **system**, not as the caller. The entity's `security {}` block gates what a PERSON
may do directly; it does not gate what a transition does once an event has been legitimately raised.

## Signature      {#signature}
```osy title="the write inside a transition is not checked against the entity's rules" syntax
state Open {
  on Close {
    this.Item.Note = "closed by the workflow";   // lands even if NO rule grants `update` to anyone
    goto Closed;
  }
}
```

## Description    {#description}

### Where the authorization actually happens   {#where}
Two different questions, and only the first one is about the person:

- **May they raise this event?** `[Authorize(principal => …)]` on the event, and a slot's `Candidates` for who may
  hold the work. See [[Authorize] (event)](https://osysharp.com/reference/workflow/authorize/).
- **May the body write this row?** Not asked. The engine opens a system data context for the run, and a system
  context is defined as one that opts out of data security.

So a transition is trusted code, in the same sense a server function's own bookkeeping is. Put the gate on the
EVENT, where the person is.

### Why it is built that way   {#why}
A run outlives the request that started it. A milestone that expires at 2am, a deadline, a retry after a service
came back — these resume with no acting principal at all, and a rule written `allow update where Owner == user`
has no `user` to compare against. If a body were gated by the caller's rules, the same transition would succeed or
fail depending on who happened to trigger it, and a timer-driven one could never succeed.

### What this means for your model   {#consequences}
- **Do not rely on `security {}` to stop a workflow.** If a field must never change once a run owns it, that is a
  guard in the body or an `[Immutable]` on the member — not a missing `allow update`.
- **A read from a page is still gated.** This is about the BODY. What the person then sees on screen goes through
  the ordinary read rules, unchanged.
- **`[Authorize]` is the real perimeter.** An event anybody may raise is an event anybody may cause every write in
  its transition to happen.

## Examples       {#examples}
A whole app, compiled: `Ticket` grants read and create and **no update to anyone**, and the transition writes
`Note` anyway.

```osy title="the body writes a field no rule grants" test app=workflow-body-security
enum TicketState { Open, Closed }

entity Ticket {
  [MaxLength(80)] string Title;
  TicketState Status = TicketState.Open;
  [MaxLength(200)] string Note = "";
  security { allow read, create when IsAnonymous || IsAuthenticated; }   // no `update`, to anyone
}

workflow TicketFlow {
  Tracks  = Ticket.Status;
  Initial = Open;

  event Close();

  state Open {
    on Close {
      this.Item.Note = "closed by the workflow";   // lands: a body is not gated by the block above
      goto Closed;
    }
  }
  state Closed { }
}

void CloseIt(Ticket t) { TicketFlow.RaiseClose(t); }
```

Gate the EVENT, which is where the person is:

```osy title="gate the EVENT, not the row" syntax
[Authorize(principal => principal == this.Item.Owner)]
event Close();
```

## See also       {#see-also}
- [[Authorize] (event)](https://osysharp.com/reference/workflow/authorize/) — who may raise an event
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — who may hold a work slot
- [security { }](https://osysharp.com/reference/security/entity-security/) — what the `security {}` block gates


---

<!-- https://osysharp.com/reference/workflow/fault-propagation/ -->

# When a child is cancelled or fails

> A workflow you waited for can end badly — cancelled, or failed. `catch` is how you handle it; not catching it is how you let it travel onward, and the run then ends at the `terminal cancel` or `terminal error` you declared. The two outcomes stay distinct however far they travel, so a workflow three levels up can still tell "the work below me was cancelled" from "it broke".

<!-- id: workflow-fault-propagation · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/fault-propagation/ -->

## Summary        {#summary}
`await Workflow.Run("child", child)` waits for a workflow to finish. If it finishes **badly** — at a `terminal cancel` or a
`terminal error` — that outcome arrives here as `WorkflowCancelled` or `WorkflowError`.

You have exactly two choices, and both are things you say in the code:

- **`catch` it** — cancellation or failure stops here, and you decide what happens next.
- **Don't** — it keeps going. This run ends at *its own* `terminal cancel` / `terminal error`, and whoever is waiting
  on *this* run receives the same kind in turn.

Not catching is a real choice, not an oversight. "If the work I'm waiting on is cancelled, I'm cancelled" is usually
exactly right, and it is the same thing C# does with a cancellation that reaches the top of a task.

## Signature      {#signature}
```osy syntax
try { await Workflow.Run("child", child); goto Done; }
catch (WorkflowCancelled e) { goto Withdrawn; }   // handle it — it stops here
catch (WorkflowError e)     { goto Rejected; }

terminal cancel Withdrawn { Message = "…"; }      // …or declare where an UNCAUGHT one lands
terminal error  Rejected  { Message = "…"; }
```

`e.Message` is the terminal's own `Message` — the sentence the workflow that ended wrote about why.

## Description    {#description}

### The two kinds mean different things, and the difference survives the trip   {#two-kinds}
`WorkflowCancelled` is a **deliberate stop**: the work was called off. `WorkflowError` is a **failure**: it went wrong.
Keeping them apart is the whole reason there are two, and it matters most far from where it happened — an approval
three workflows up should react differently to "the customer withdrew" than to "the payment provider rejected it".

So the kind is preserved every step of the way. A cancellation that travels up four runs is still a cancellation when
it gets there, and the fourth run's `catch (WorkflowCancelled e)` is what fires.

### Where an uncaught one lands, and why you have to say   {#uncaught}
An uncaught outcome ends the run at the terminal you declared for it — a **real transition**: the tracked property
moves, the audit records it, and the terminal's `Message` is what the next run up receives.

That is why the compiler asks you to declare one. If a wait can be cancelled and nothing catches it, the run is going
to end there, and a run has to end *somewhere you named*. The rule is narrow on purpose — you are only asked when
**both** are true:

- nothing catches it on that path, **and**
- the workflow you are waiting on can actually produce that outcome.

A child that declares no `terminal cancel` can never be cancelled, so you are never asked to declare a cancel terminal
for it. You will not be made to write states that can't be reached.

Declaring **two** terminals of the same kind is also an error: an uncaught outcome would have two places to land, and
the compiler will not guess. Keep one, or catch it and `goto` the one you mean.

### You cannot raise these yourself   {#platform-only}
`WorkflowCancelled` and `WorkflowError` are raised by the platform, and only by the platform. `throw`ing one is a
compile error.

They mean one precise thing — *a workflow I waited for ended at a declared terminal* — and everything above depends on
that staying true. To end **your own** workflow that way, `goto` its terminal: that is declared, audited, moves the
tracked property, and carries a `Message`. To fail an ordinary function, `throw new Exception("…")`.

## Examples       {#examples}
A child that can end all three ways, and a parent that handles one of them and lets the other travel:

```osy title="the workflows" test app=wf-fault-propagation-example
enum OrderStatus { Placed, Shipped, Rejected, Withdrawn }
enum PickStatus  { Waiting, Picked, Empty, Cancelled }

entity Order { [Required, MaxLength(40)] string Reference; OrderStatus Status = OrderStatus.Placed; }
entity Pick  { [Required] Order Order; PickStatus Status = PickStatus.Waiting; }

workflow PickFlow {
  Tracks = Pick.Status; Autostart = false; Initial = Waiting;
  event Report(bool inStock);
  state Waiting {
    subscribe Report(bool inStock);
    on Report(bool inStock) {
      when (inStock) { goto Picked; }
      default { goto Empty; }
    }
  }
  terminal success Picked    { }
  terminal error   Empty     { Message = "out of stock"; }
  terminal cancel  Cancelled { Message = "picking called off"; }
}

workflow OrderFlow {
  Tracks = Order.Status; Autostart = false; Initial = Placed;
  state Placed {
    enter {
      var pick = Workflow.Once("make-pick", () => new Pick { Order = this.Item });
      try { await Workflow.Run("pick", pick); goto Shipped; }
      catch (WorkflowError e) { goto Rejected; }     // a FAILURE stops here — we reject the order
    }
  }
  terminal success Shipped   { }
  terminal success Rejected  { }
  // A CANCELLATION is deliberately not caught: if picking was called off, the order is withdrawn. This is the
  // terminal that says where it lands — and without it, the code above would not compile.
  terminal cancel  Withdrawn { Message = "order withdrawn"; }
}
```

Read the `Placed` state as a sentence: *ship it if the pick succeeds; reject it if the pick fails; if the pick is
called off, so is the order.* The third clause is the terminal, not a `catch`.

## Notes          {#notes}
- **Only these two outcomes behave this way.** An ordinary failure — a dropped connection, a timeout, a bug — is not a
  workflow outcome, and the platform retries it rather than ending the run, because another attempt may well succeed.
  A cancelled or failed child is different: it has already finished, so trying again would read the same answer.
- **Compensations still run.** If the run had a [Workflow.BeginSaga (a compensating saga scope)](https://osysharp.com/reference/workflow/beginsaga/) scope open, leaving through an uncaught outcome
  disposes it on the way out, so the work it registered is undone before the run ends.
- **A `catch (Exception e)` catches these too**, like any other. Reach for the specific ones when the difference
  between "called off" and "went wrong" changes what you do.

## See also       {#see-also}
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — start a workflow, and optionally wait for it.
- [Workflow.BeginSaga (a compensating saga scope)](https://osysharp.com/reference/workflow/beginsaga/) — undoing work when a wait ends badly.
- [Parallel legs (start several, then wait for them)](https://osysharp.com/reference/workflow/parallel-legs/) — waiting on several at once, and what happens when one of them fails.


---

<!-- https://osysharp.com/reference/workflow/beginsaga/ -->

# Workflow.BeginSaga (a compensating saga scope)

> Open a saga scope that couples each forward step with its compensation. `await saga.Run("step", step, () => Undo(…))` runs a child-workflow step and registers its undo in one call; if a later step fails, disposing the scope without `saga.Complete()` runs the registered undos in reverse. It is the C# `TransactionScope` idiom — `await using` + a durable undo stack — for a multi-step saga whose plain `try`/`catch` would otherwise force a compensation staircase.

<!-- id: workflow-beginsaga · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/beginsaga/ -->

## Summary        {#summary}
**`Workflow.BeginSaga()`** opens a **saga scope** — a durable transactional scope for a **multi-step** saga. Its one
load-bearing idea is **coupling**: `await saga.Run("step", step, () => Undo(step))` runs a step **and** registers its
compensation in a single call, so a step can never run without its undo. If any later step fails, disposing the scope
**without** calling `saga.Complete()` runs every registered undo **in reverse order**.

Reach for it only for the **complex** case. A **simple** saga — one step, one compensation — is cleaner as a plain
`try`/`catch` around [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) (nothing to couple). A saga with N steps that must unwind in reverse is where
pure `try`/`catch` forces a **compensation staircase** — each step's `catch` re-listing every earlier undo — and that
duplicated, hand-ordered list is exactly the bug-farm the scope removes.

## Signature      {#signature}
```osy syntax
await using var saga = Workflow.BeginSaga();   // open the scope; dispose-without-Complete unwinds, in reverse
await saga.Run("step", step, () => Undo(args));        // run a child-workflow step AND register its compensation
await saga.Run("step", step);                          // a step with no compensation (e.g. the last one)
saga.OnUnwind(() => Undo(args));               // register a compensation with NO forward step
saga.Complete();                               // every step stood → drop the undo stack (dispose becomes a no-op)
```

`step` is an entity-typed value whose type has exactly one workflow bound to it — the same inference [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/)
uses. A compensation lambda's body is a single call: an app **function** (`() => VoidAuth(auth)`) or a compensating
**child workflow** (`() => Workflow.Run(new ReturnShipment { Shipment = s })`).

## Description    {#description}
`await using var saga = Workflow.BeginSaga();` is a C# **using declaration**: the scope is disposed at the end of the
enclosing block, on **every** exit path — normal fall-through, a `goto` to a terminal, or a thrown fault. Disposal runs
the saga's registered compensations **unless** `saga.Complete()` was reached first.

**Each step registers its undo, and only committed steps compensate.** `await saga.Run("step", step, () => Undo(step))` starts
the child workflow bound to `step` and **waits** for it (a durable wait — the flow parks and resumes exactly like an
awaited [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/)). On the step's **success**, its undo is pushed onto the scope's stack; on a **failure**, the
call throws **`WorkflowError`** (or **`WorkflowCancelled`**) and registers **nothing** — a step that never committed is
never compensated. So a failing step propagates its fault to your `catch`, and the scope unwinds the steps that *did*
stand.

**Reverse order is automatic, and disposal runs before the terminal transition.** The undo stack is LIFO, so
compensations run newest-first. Because a workflow `goto` is deferred to the end of the body, the `await using`
disposal runs its compensations **before** the routing `goto` takes effect — you compensate, then route, with no
explicit unwind call.

**A compensation is a function or a child workflow.** Same step-granularity rule as a forward step: a DB-only or
single-call undo is an app **function** the saga names; a multi-step, waiting undo (a real return: pickup → inspect →
refund) is a **child workflow** (`() => Workflow.Run(new Return { … })`). A child-workflow undo **waits** too — disposal
parks on it and resumes when it terminals, then continues unwinding the rest of the stack.

**`saga.OnUnwind(() => Undo(…))`** registers a compensation that has **no** forward step — useful when something you did
outside a `saga.Run` (a side effect earlier in the body) still needs undoing if the saga rolls back. It pushes onto the
same stack, in call order.

**`saga.Complete()`** marks the saga successful: it drops the undo stack, so the `await using` disposal becomes a no-op.
Call it once every step has stood, just before you route to the success terminal.

**The scope is durable.** The undo stack rides the parked continuation, so a saga that waits across steps (or across a
restart) resumes with its registered compensations intact.

## Examples       {#examples}
A four-step fulfillment saga, shown as the complete app the docs gate compiles. Each step is a child workflow that waits
on the outside world; the first three couple a plain-function compensation, the shipment step's compensation is itself a
child workflow (a real multi-step return), and `saga.OnUnwind` registers one more undo that has no forward step. If any
step fails, the committed steps unwind in reverse and the order routes to the error terminal.

First the world the saga runs in — the order and its customer, the four workflow-bound step entities, the compensating
`ReturnShipment` child, and the compensation functions the saga names:

```osy title="the app the saga lives in" test app=wf-beginsaga-example
enum OStatus     { Received, Shipped, Rejected, Cancelled }
enum StepStatus  { Waiting, Done }

entity Customer { [Required, MaxLength(60)] string Name; decimal LastRefund; }
entity Order    { [Required] Customer Customer; decimal Total; OStatus Status = OStatus.Received; }

entity PaymentAuth          { [Required] Order Order; decimal Amount; StepStatus Status = StepStatus.Waiting; bool Undone; }
entity InventoryReservation { [Required] Order Order; StepStatus Status = StepStatus.Waiting; bool Undone; }
entity WarehousePick        { [Required] Order Order; StepStatus Status = StepStatus.Waiting; bool Undone; }
entity Shipment             { [Required] Order Order; StepStatus Status = StepStatus.Waiting; }
entity ReturnShipment       { [Required] Shipment Shipment; StepStatus Status = StepStatus.Waiting; }

void VoidAuth(PaymentAuth auth)                        { auth.Undone = true; }
void ReleaseReservation(InventoryReservation reserve)  { reserve.Undone = true; }
void CancelPick(WarehousePick pick)                    { pick.Undone = true; }
void RevokeLoyalty(Customer customer, decimal amount)  { customer.LastRefund = amount; }

workflow PaymentAuthFlow { Tracks = PaymentAuth.Status; Autostart = false; Initial = Waiting;
  event Finish(); state Waiting { subscribe Finish(); on Finish { goto Done; } } terminal success Done { } }
workflow InventoryReservationFlow { Tracks = InventoryReservation.Status; Autostart = false; Initial = Waiting;
  event Finish(); state Waiting { subscribe Finish(); on Finish { goto Done; } } terminal success Done { } }
workflow WarehousePickFlow { Tracks = WarehousePick.Status; Autostart = false; Initial = Waiting;
  event Finish(); state Waiting { subscribe Finish(); on Finish { goto Done; } } terminal success Done { } }
workflow ShipmentFlow { Tracks = Shipment.Status; Autostart = false; Initial = Waiting;
  event Finish(); state Waiting { subscribe Finish(); on Finish { goto Done; } } terminal success Done { } }
workflow ReturnShipmentFlow { Tracks = ReturnShipment.Status; Autostart = false; Initial = Waiting;
  event Finish(); state Waiting { subscribe Finish(); on Finish { goto Done; } } terminal success Done { } }
```

Then the saga itself — one `await using` scope in the order workflow's `enter` body, coupling each step with its undo:

```osy title="a four-step fulfillment saga" test app=wf-beginsaga-example
workflow OrderFlow {
  Tracks = Order.Status; Autostart = false; Initial = Received;
  state Received {
    enter {
      await using var saga = Workflow.BeginSaga();          // dispose-without-Complete unwinds, in reverse
      try {
        var auth = Workflow.Once("make-auth", () => new PaymentAuth { Order = this.Item, Amount = this.Item.Total });
        await saga.Run("auth", auth, () => VoidAuth(auth));          // run step 1 + register its undo, atomically

        var reservation = Workflow.Once("make-reservation", () => new InventoryReservation { Order = this.Item });
        await saga.Run("reservation", reservation, () => ReleaseReservation(reservation));

        var pick = Workflow.Once("make-pick", () => new WarehousePick { Order = this.Item });
        await saga.Run("pick", pick, () => CancelPick(pick));

        var shipment = Workflow.Once("make-shipment", () => new Shipment { Order = this.Item });
        await saga.Run("shipment", shipment, () => Workflow.Run(new ReturnShipment { Shipment = shipment }));  // undo = a child workflow

        saga.OnUnwind(() => RevokeLoyalty(this.Item.Customer, this.Item.Total));   // an undo with no forward step

        saga.Complete();                                     // every step stood → drop the stack
        goto Shipped;
      } catch (WorkflowError e)     { goto Rejected; }        // dispose unwinds the committed steps (reverse), then routes
        catch (WorkflowCancelled e) { goto Cancelled; }
    }
  }
  terminal success Shipped   { }
  terminal error   Rejected  { Message = "order rejected"; }
  terminal error   Cancelled { Message = "order cancelled"; }
}
```

## Notes          {#notes}
- **Simple vs complex.** One step, one compensation → a plain `try`/`catch` around [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) is cleaner; the
  scope earns its place only when N steps must unwind in reverse (it removes the compensation staircase).
- **Couple the undo with the step.** `await saga.Run("step", step, undo)` is the point — you cannot run a step and forget its
  compensation, and you never hand-maintain a reverse-ordered undo list.
- **Only committed steps compensate.** A step that throws registered no undo, so it is not compensated; the steps
  before it are.
- **`await` is the wait.** `await saga.Run("step", ...)` and `await saga.DisposeUnwind()` (the disposal the `await using`
  generates) are durable waits — the flow parks and resumes, even across a restart. `saga.OnUnwind` and `saga.Complete`
  are not waits.
- **`Complete()` or it rolls back.** Reaching `saga.Complete()` is what commits the saga; any exit before it (a fault,
  or a `goto` out of the block) runs the compensations.

## See also       {#see-also}
- [Workflows that outlive the code that started them](https://osysharp.com/reference/workflow/change-over-time/) — what a deploy does to a saga parked halfway through its steps
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — start a workflow on an entity; the awaited form is a single durable step (the simple-saga case).
- <span class="planned" title="this page is planned and not written yet">workflow-goto</span> — transition within a workflow body (deferred to body-end, which is why disposal compensates first).
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — read a workflow's timeline of events.
- [Step labels (naming a child run so it survives a new version)](https://osysharp.com/reference/workflow/park-label/) — naming a step so a run parked on it survives a new version of this saga.


---

<!-- https://osysharp.com/reference/workflow/inbox/ -->

# Workflow.Inbox&lt;T&gt; (what is waiting for me)

> The current principal's queue: every slot they can act on, across every run of every workflow that tracks T. Rows carry the tracked entity typed, so a screen renders it without a read per row.

<!-- id: workflow-inbox · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/inbox/ -->

## Summary        {#summary}
Every other way of reading a workflow starts from one row: *what is the state of this invoice?* An inbox asks the
opposite question — *what is waiting for me?* — and it crosses every run.

**`Workflow.Inbox<T>()`** answers it for whoever is asking right now.

## Signature      {#signature}
```osy syntax
Workflow.Inbox<TrackedEntity>()
```
No arguments: it is the CURRENT principal's queue. Filter, order and count it like any other list.

## Description    {#description}
### What is in it   {#contents}
A slot is in your queue when it is **assigned to you**, or it is **open and you satisfy its `Candidates`**. Slots
that are waiting on something else are included too, carrying their status — so a screen can grey out *"CFO, waiting
for Manager"* rather than hiding work that is coming.

`T` is the entity a workflow **`Tracks`**. Asking for an entity no workflow drives is a compile error, because a
queue is a thing a workflow produces.

### The row   {#row}
| member | what it is |
|---|---|
| `Item` | the tracked entity, **typed** — an `Invoice`, not an id |
| `SlotAlias` | which slot is asking |
| `WorkflowName` | the workflow it belongs to |
| `Status` | the slot's live status |
| `OpenedAt` / `BreachesAt` | when it arrived, and when it runs out of time |
| `RunId` / `SlotId` | the run and the slot themselves |

**The alias is not decoration.** *"Waiting on you as their manager"* and *"waiting on you as finance"* are different
asks, and the same person can hold both on different items. A queue that could not tell them apart would be showing
one list where there are two.

**`BreachesAt` is the sort key.** For a workflow that rejects on breach, *"3 hours left"* is the most actionable
thing on the row. It is null when a slot has no deadline — absence, not a far-future date to filter around.

**`OpenedAt` is when the slot opened, not when the run reached the state.** For a slot held closed by
[`After`](https://osysharp.com/reference/workflow/slot-dependencies/) those are different moments — it waits `Pending` while its predecessors run —
and the row reports the later one. So *"waiting since"* means waiting **on you**, and it agrees with `BreachesAt`,
whose clock starts at the same moment.

### Reading through `Item`   {#item}
`Item` is the tracked entity, so a screen reads it directly — that is the point of the queue being typed rather than
a list of ids. Navigating it needs it [included](https://osysharp.com/reference/query/include/), as any reference does:

```osy title="the morning screen" test app=workflow-inbox
enum ClaimStage { Filed, Approved, Rejected }

[Principal] entity Employee {
  [Required] [MaxLength(80)] string DisplayName;
  security {
    allow read   when IsAuthenticated;   // a principal row is not public — say who may read it
    allow create when IsAuthenticated;
  }
}

entity Invoice {
  [Required] [MaxLength(120)] string Title;
  [Required] decimal Amount;
  [Required] Employee Owner;
  ClaimStage Stage;
  security {
    allow read, update when IsAuthenticated;
    allow create       when IsAuthenticated;
  }
}

workflow ExpenseApproval {
  Tracks    = Invoice.Stage;
  Autostart = true;
  Initial   = Filed;

  event Decide(bool approved);

  state Filed {
    subscribe Decide(bool approved) as Manager { Assignee = this.Item.Owner; }
    on Manager(bool approved) {
      when (approved) { goto Approved; }
      default { goto Rejected; }
    }
  }

  terminal success Approved { }
  terminal error   Rejected { Message = "rejected"; }
}

int WaitingOnMe() {
  return Workflow.Inbox<Invoice>()
                 .Include(r => r.Item.Owner)
                 .OrderBy(r => r.BreachesAt)
                 .Count();
}
```

### Whoever is asking   {#principal}
The queue is defined against the **current principal**, and it follows `runas`. That is deliberate: a workflow can
put an agent in a slot as readily as a person, so *"what is waiting on this principal"* stays one question rather
than growing a second surface for the non-human case.

An anonymous caller has an empty queue — nothing is waiting on nobody. That is an answer, not an error, so a public
page still renders.

### It is live   {#live}
The queue is re-read, not remembered. Act on a slot and it leaves your queue; it does not linger as a row that does
nothing when clicked. One person acting does not change anyone else's queue.

### Answering a row   {#answering}
[`Workflow.Deposit(row, Decide(true))`](https://osysharp.com/reference/workflow/inbox-act/) answers the slot a row is, and `Workflow.Claim(row)` /
`Workflow.Release(row)` take and hand back unassigned work. The event is named at the call site because a queue's rows
are heterogeneous — which slot a row turned out to be is known only once the queue has been read.

## Examples       {#examples}
Only the work of one kind, most urgent first:

```osy title="one queue, one question" syntax
Workflow.Inbox<Invoice>()
  .Include(r => r.Item)
  .Where(r => r.SlotAlias == "Finance")
  .OrderBy(r => r.BreachesAt)
```

## Notes          {#notes}
**Authorization is the workflow's, not the queue's.** Membership is decided by the same `Candidates` evaluation that
governs claiming and depositing — the queue does not get its own rules. A queue is exactly the place a second,
looser answer would otherwise appear, and there is deliberately nowhere for one to live.

**It runs on the server.** Deciding what a principal may act on is not a question a client can be trusted to answer
about itself.

## See also       {#see-also}
- [Workflow.WorkByItem&lt;T&gt; (one row per item — the board read)](https://osysharp.com/reference/workflow/work-by-item/) — one row per ITEM rather than per slot: the BOARD read, with the clock that governs across an item's slots
- [Tracks and Initial (the field a workflow drives)](https://osysharp.com/reference/workflow/tracks/) — the `Tracks` declaration that makes a queue exist at all
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — who is eligible for a slot
- [Include (pre-loading relations)](https://osysharp.com/reference/query/include/) — loading `Item` before you read through it


---

<!-- https://osysharp.com/reference/workflow/once/ -->

# Workflow.Once (run a step at most once)

> Run something at most once per workflow run, however many times the surrounding code re-executes. The first execution runs the step and records its result durably; every later execution of that call site returns the recorded result without running the step again. Reach for it around anything that leaves the platform — a payment, a message, an outbound call — where running twice would be worse than running slowly.

<!-- id: workflow-once · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/once/ -->

## Summary        {#summary}
A workflow body can run more than once. That is not a bug — it is how the platform survives a crash: work that did not
commit is simply done again. For ordinary computation that is exactly right. For a **payment**, an **email**, or any
call into a system that is not yours, it is a second charge and a second message.

**`Workflow.Once("step", () => step)`** marks the boundary. The first time a run reaches that call site, the step runs and
its result is recorded durably. Every later time — a retry after a crash, a resume after a wait — the recorded result
comes straight back and **the step does not run**.

## Signature      {#signature}
```osy syntax
Workflow.Once("charge", () => step)              // this step's LABEL, then the step: run at most once
Workflow.Once("charge", key => step)            // …and receive an idempotency key to pass on
Workflow.Once("charge", order.Id, key => step)  // …with that key scoped to the order, not to this run

Workflow.Once("charge", () => step,             // …and try again if it FAILS, on a growing wait
              retry: Backoff.Exponential(TimeSpan.FromSeconds(2)).MaxAttempts(4))
```

The argument is a **lambda**, and that is load-bearing: the step must not run until `Once` has checked whether it
already did. Writing `Workflow.Once("charge", Charge(total))` would evaluate `Charge` first — the very thing being prevented — so
it is a compile error.

The result type is the lambda's own, so `Once` can be wrapped around an existing expression without changing anything
around it.

## Description    {#description}

### What it guarantees, precisely   {#guarantees}
- **The result is recorded once and reused.** A re-execution returns the first result, so everything downstream sees
  the same value it saw the first time and cannot diverge.
- **A step that returned nothing still counts as run.** A recorded empty result is a hit, not a miss — a notification
  that returns nothing does not send twice.
- **The record survives a rollback.** It is committed separately from the surrounding work, on purpose: if it were
  written with the rest of the step's transaction, a crash before that transaction committed would take the record down
  with it and the retry would run the step again.

### What it does not guarantee   {#limits}
**A crash in the instant between the step returning and its result being recorded will run the step again.** That
window is small, but it is real, and no workflow engine can close it — the effect happened in someone else's system,
and there is no way to know it happened without a record of it. Every durable engine draws the line in the same place.

Closing it takes the other side's help, in the form of an **idempotency key** it can use to recognise a retry. Take one
by giving the lambda a parameter:

```osy syntax
var receipt = Workflow.Once("charge", key => Payment.Charge(total, idempotencyKey: key));
```

The key is derived for you, and it is the same key every time this step of this run is attempted — which is exactly
what makes the receiving system able to spot the second attempt. It is unique per step and per run, so a loop over
three lines presents three keys and a second order presents different ones again. It is opaque: it carries nothing
about your code.

`Once` gives you a stable result; the key gives you a single effect.

### `retry:` — trying again when the step FAILS {#retry}
`Once` is about a step not repeating. **`retry:` is about a step that did not succeed at all.** They are opposite
halves of the same question and they compose: a retried step still records its result the first time it works, and
still returns that record for ever after.

```osy title="retrying a step that FAILED, not one that repeated" syntax
Workflow.Once("charge", () => Payment.Charge(total),
              retry: Backoff.Exponential(TimeSpan.FromSeconds(2)).MaxAttempts(4))
```

Without `retry:`, a step that throws ends the body — the fault travels out to whoever drove the run (the person who
clicked the button, the timer that fired) and the run stays where it is. With it, the run **parks on a durable clock**
and runs the body again when the wait elapses, up to the policy's `.MaxAttempts`.

The wait is durable rather than a pause, and that is not an implementation detail you can ignore: the body runs
inside whatever request drove it, so a five-minute backoff taken in-process would hold a person's browser open for
five minutes. Parking means the caller returns immediately and the run resumes on its own.

**`.MaxAttempts(n)` is required here**, unlike on a milestone — a milestone has `Retries = N` counting for it, and a
step has nothing else in scope, so an uncapped policy would retry for ever:

```console
a step's `retry:` policy must cap its attempts with `.MaxAttempts(n)` — without one the step would be retried
for ever. A milestone can leave it out because its `Retries = N` supplies the budget; a step has nothing else in
scope. Write `Backoff.Exponential(TimeSpan.FromSeconds(2)).MaxAttempts(4)` — that is 4 attempts in TOTAL, so 3
retries after the first try.
```

#### What a retry re-runs, and what it does not   {#retry-scope}
The retry **re-enters the body from the top**, rather than resuming at the step. That sounds like more work than
necessary and is the only shape that is correct: a step that failed halfway may have written rows, and resuming at
the step would have to commit them. So the failed attempt's writes are discarded entirely, the body runs again, and
**every step that already completed returns its record instead of running** — which is exactly what `Once` is for.
Work between the steps is re-executed by the same code that did it the first time.

⚠ So `retry:` and `Once` are not independent decorations. A retried body that does effectful work *outside* a step
will do that work again on every attempt. Put anything that must not repeat inside its own `Once`.

#### Which failures are retried   {#retry-which}
Everything except the ones that cannot succeed on a second attempt: a row that was **not found**, a value that
**failed validation**, a **conflict**, an **authorization refusal**, an **unmet requirement**, and a **cancelled**
child workflow. Those are the platform having decided something, and waiting changes none of them.

There is no predicate to write. Classifying exceptions at the call site would be a second way to spell `catch`, in a
position where you cannot see the body it guards — so `retry:` stays a single value, and anything more specific is
written with the `try`/`catch` you would already reach for.

#### A retried step may not `await`   {#retry-no-await}
A retry parks on a **clock** and re-runs the body from the top; an `await` parks on a **cursor** and resumes where it
left off. One run cannot hold both waits, so a step carrying `retry:` whose body awaits is a compile error rather
than a policy that quietly stops applying the moment the body suspends. Let the child workflow carry its own retry,
or move the awaited call out of the step.

#### `try`/`catch` sees the LAST failure, not each one   {#retry-catch}
A `catch` around a retried step is asking *what to do when this has failed for good*. It does not run per attempt: the
retry happens inside the step, and only when the attempts are spent does the original exception come out — at which
point your handler sees exactly what it would have seen had no policy been written.

### When the retry is a whole new run   {#new-run}
The key above is scoped to **this run**, which is right for "this run's step is being attempted again". It is not what
you want when the second attempt is a *different* run against the same thing — a failed order retried tomorrow, a
re-submitted invoice. Those are two runs, so they derive two keys, and the charge happens twice.

Say what the work is really identified by, as an ordinary first argument, and **name the step**:

```osy title="scoping the key to the order, so a second run matches" syntax
Workflow.Once("charge", order.Id, key => Payment.Charge(order.Total, idempotencyKey: key));
```

Now any run reaching that step for **that order** presents the same key, and the receiving system recognises the
second one. Two things stay true and are worth being precise about:

- **The step still runs.** Scoping changes the key, not the record — a record belongs to a run and is cleaned up with
  it. What you gain is that the far side refuses to act twice, which is the only place a single effect can be decided.
- **Different steps under one scope still get different keys.** Otherwise a refund under the same order would present
  the charge's key and be swallowed as a duplicate — a payment that never happens, which is worse than one that
  happens twice, because nothing reports it.

### Why the scoped form makes you name the step   {#scoped-name}
The name is not decoration, and it is the reason the two bullets above can both be true. Something has to tell the
`"charge"` step apart from the `"refund"` step under the same order, or they derive one key and the refund is
swallowed.

That job used to be done by **where the step sat in your code** — which worked until you edited the function around
it. Then the key changed, and a run started after the edit was no longer recognised as a retry of one started before:
exactly the case a scope exists for, since two runs far enough apart to matter will usually straddle a deploy. A name
you chose does the same job and survives the edit, so the promise holds across versions. That is why the scoped form
requires one and the plain form has no use for it.

Pick names that describe the work — `"charge"`, `"send-invoice"` — and treat them as durable: **renaming one is a new
key**, and a run mid-flight against the old name will not recognise the new one.

### Where it can go   {#where}
Anywhere in a workflow body: a state's `enter`, an event arm, a timer body, a saga step, or a function the body calls.
Inside any loop — `foreach`, `while`, `for` — each iteration is its own step, so a loop that charges once per line
charges once per line, and a retry re-charges none of the lines it already did.

A step is identified by the **path taken to reach it**, not only by the line it sits on — the body the run entered,
then the name of each function called along the way, then the step's own label. An operator reading a run's records
sees exactly that: `Approved.enter/Charge#charge-card`.

Every part of it is a name you chose, so editing the code around a call does not move it. That is what lets a run
that started before a deploy recognise the work it already did. The one exception is a loop, which contributes the
iteration number — there is nothing in your source to name there, and "which pass over the data" is the honest
identity.

Because a path segment is a *name*, calling one helper **twice from the same body** would give the steps inside it
one identity, so that is a compile error. Loop over the things you are acting on and each pass is its own step; or,
if the two calls are really different work, give them their own functions.

(That path identifies a step's *record*. A scoped step's outward idempotency key is identified by its label
instead — see above.)

### Every step carries a label {#label}
The first argument is the step's **label** — a literal string you choose, naming what this step does. It is required.

The label is the step's **durable identity**. The platform records "this step already ran" against it, so the label is
how a later execution finds that record. It used to be derived from where the step sat in the code, which worked
within one deployed version and broke across a new one: edit anything above the step and its identity moved, the
record was no longer found, and the step ran a second time. For a payment or a message, a second run is the whole
problem this construct exists to prevent.

A name you chose does not move when you edit the code around it. That is the entire reason it is not optional.

```osy syntax
Workflow.Once("charge-card", key => Payments.Charge(total, idempotencyKey: key));
Workflow.Once("send-receipt", () => Email.Receipt(order));
```

Two steps in the **same body** may not share a label — they would share one identity, and the second would read
back the first's recorded result and never run. That is a compile error.

A **body** here means one block the platform runs as a unit: a function, or one of a workflow's own — its `Start`, a
state's `enter`, a single event arm, a milestone hook, a reminder. Each is a separate identity, so the same label in
a state's `enter` and in an event arm is two different steps, and is fine. So is the same label in two different
functions. Labels are compared exactly.

### You do not have to write one — until two calls collide {#unlabelled}
Most external calls need no `Once` at all. Write the call plainly and the platform wraps it in a step for you, naming
that step after the call:

```osy title="one plain call needs no label at all" syntax
Mailer.Send(receipt);          // one step, named for `Mailer.Send`. Nothing to write, nothing to maintain.
```

That name is a complete identity while there is **one** such call in the body — nothing about `Mailer.Send` moves,
however much you edit around it. Two calls to the *same* operation in one body have no way to be told apart, so the
compiler asks you to name them:

```osy title="✗ two calls to the same operation must be named" syntax
Mailer.Send(primary);          // ✗ two unlabelled steps, both named for `Mailer.Send`
Mailer.Send(backup);
```
> two external steps here both go through `Mailer.Send`, so neither carries an identity that survives an edit …
> Name them: `Workflow.Once("a-name-for-this-one", () => Mailer.Send(…))`

Name either one and the ambiguity is gone — the labelled step takes your name and the remaining plain call keeps the
derived one. Calls to *different* operations never collide, so they need nothing.

**Why it is refused rather than guessed at.** Numbering them by position works right up to the edit that matters:
insert an earlier call to the same operation and the later one renumbers, so a run resuming after that deploy no
longer recognises the step it already did — and sends twice. That is the failure this whole construct exists to
prevent, so the platform declines to guess.

## Examples       {#examples}
Charging a card — the case the construct exists for:
```osy title="charging a card — the case the construct exists for" syntax
state Approved {
  enter {
    var receipt = Workflow.Once("charge", () => Payment.Charge(this.Item.Total));
    this.Item.ReceiptId = receipt.Id;
    goto Shipped;
  }
}
```
If the process dies after the charge but before `goto Shipped` commits, the run retries `enter` from the top — and the
charge does not happen again.

One step per item — and the same in any loop:
```osy title="a loop gives every line its own step" syntax
foreach (var line in this.Item.Lines) {
  Workflow.Once("reserve", () => Fulfilment.Reserve(line.Sku, line.Quantity));
}
```
Each line is its own step: a retry re-reserves none of the lines already reserved, and still reserves the rest.

A step that has to survive a flaky dependency — the whole workflow, compiled:

```osy title="a durable step with a retry policy" test app=workflow-once-retry
enum OrderStage { Placed, Charged, Failed }

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

entity Order {
  [Required, MaxLength(120)] string Reference;
  OrderStage Stage;
  decimal Total;
  [MaxLength(200)] string ReceiptId;
  security { allow read, create, update when IsAuthenticated; }
}

// Stands in for the payment gateway. Anything that can be briefly unavailable belongs behind a step.
string Charge(Order order) {
  return "receipt-" + order.Reference;
}

workflow OrderFlow {
  Tracks    = Order.Stage;
  Autostart = true;
  Initial   = Placed;

  event Pay();

  state Placed {
    subscribe Pay();
    on Pay {
      // Four attempts in total — 2s, then 4s, then 8s — and never twice on success, because it is still a step.
      this.Item.ReceiptId = Workflow.Once("charge", () => Charge(this.Item),
                                          retry: Backoff.Exponential(TimeSpan.FromSeconds(2)).MaxAttempts(4));
      goto Charged;
    }
  }

  terminal success Charged { Message = "paid"; }
  terminal error   Failed  { Message = "could not charge"; }
}
```

Skipping expensive work that is merely slow:
```osy title="skipping work that is slow rather than external" syntax
var report = Workflow.Once("build-quarterly-report", () => BuildQuarterlyReport(this.Item));
```
Nothing here leaves the platform, so re-running would be *correct* — just wasteful. `Once` is also how you say "do not
redo this."

## Notes          {#notes}
- **A recorded step can outlive work that was rolled back.** If the surrounding transaction rolls back after the step
  escaped, the record still says it ran — because it did. Surprising once; correct every time.
- **`Once` is about repetition, not waiting.** It does not pause the run and is not a substitute for
  [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/)'s durable wait. A step that itself waits can be wrapped in one.
- **Records belong to the run** and are cleaned up with it. That stays true even with a scope: what a scope shares
  across runs is the KEY, never the record — so two runs doing the same work each still do it once, and it is the
  receiving system that declines the second.
- **It composes with app versions.** A recorded result is data belonging to the run, so it is unaffected by a
  redeploy — see [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/).

- **You often do not need to write it.** A call that leaves the platform is made a step by the compiler — see
  [Automatic durability (steps you do not have to write)](https://osysharp.com/reference/workflow/automatic-durability/). Writing `Once` around one of those gives you one step, not two. Reach for the
  explicit form when you want an idempotency key, your own boundary, or to skip expensive-but-harmless work.

## See also       {#see-also}
- [Backoff (retry policy)](https://osysharp.com/reference/workflow/backoff/) — the retry policy `retry:` takes, and what `.MaxAttempts` / `.Cap` / `.Jitter` mean
- [Workflows that outlive the code that started them](https://osysharp.com/reference/workflow/change-over-time/) — how a step's label, its memo and its call path survive a deploy
- [Automatic durability (steps you do not have to write)](https://osysharp.com/reference/workflow/automatic-durability/) — the steps the compiler writes for you, and when to write your own.
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — start a workflow, and optionally wait for it.
- [Workflow.BeginSaga (a compensating saga scope)](https://osysharp.com/reference/workflow/beginsaga/) — compensating steps, for work that must be *undone* rather than not repeated.
- [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/) — what a run in flight keeps executing across a deploy.


---

<!-- https://osysharp.com/reference/workflow/retarget/ -->

# Workflow.Retarget (re-base the SLA clocks)

> Re-evaluates every SLA clock's budget on the current run against the now-updated entity, so a mid-run change to the SLA terms (e.g. re-grading a ticket's severity) takes effect from the change, not from the origin. Call it after writing the new terms in a handler body.

<!-- id: workflow-retarget · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/retarget/ -->

## Summary        {#summary}
**`Workflow.Retarget()`** re-evaluates every live SLA clock on the current run — the whole-instance `Deadline`, the
current state's `Expire`, and each milestone's `Within` — against the entity as it stands *now*. Use it when a handler
body has just changed the values those budgets are computed from (a severity re-grade, a new contract), so the new,
possibly tighter, terms apply immediately. The time already accrued is kept; only the remaining budget is re-based from
the change point. It is a no-argument, fire-and-forget effect and returns nothing.

## Signature      {#signature}
```osy syntax
Workflow.Retarget();
```

## Description    {#description}
A workflow's SLA budgets are expressions over `this.Item` (`Expire = this.Item.SlaResolveWithin`, a milestone
`Within = this.Item.Foo`). They are evaluated once, when the clock is armed. If a handler later rewrites those inputs,
the armed clocks still hold the OLD budget — `Workflow.Retarget()` is how you make them reflect the new one.

For each live clock it: re-evaluates the clock's budget expression against the current `this.Item`; re-resolves the
run's [ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/) schedule (which the same handler may also have changed); keeps the SLA time accrued so
far; and re-bases the **remaining** budget from now (`remaining = newBudget − accrued`, walked through the new
schedule). If the new budget is already exhausted the clock is due immediately. A **paused** clock (the run is in a
non-accruing state) keeps its new budget and re-bases when it resumes.

`Workflow.Retarget()` is only valid inside a workflow handler body (it acts on the enclosing run). It runs on the same
transaction as the body, so the new terms and the re-based clocks commit together.

## Examples       {#examples}
Re-grade a ticket and re-base its clocks in one route body:

```osy title="escalate then retarget" test app=workflow-retarget
enum Severity { Low, High }
enum TicketState { Open, Done }

entity SlaTarget {
  [Required] Severity Severity;
  TimeSpan RespondWithin;
  security { allow read, create when IsAuthenticated; }
}

entity Ticket {
  [Required, MaxLength(120)] string Title;
  Severity Severity = Severity.Low;
  TimeSpan SlaRespondWithin;
  TicketState State = TicketState.Open;
  security { allow read, create, update when IsAuthenticated; }
}

workflow TicketFlow {
  Tracks  = Ticket.State;
  Initial = Open;
  event Bump(Severity severity);
  state Open {
    subscribe Bump(Severity severity);
    on Bump(Severity severity) {
      this.Item.Severity         = severity;
      var target = SlaTarget.Single(t => t.Severity == severity);
      this.Item.SlaRespondWithin = target.RespondWithin;
            Workflow.Retarget();                                // the new terms take effect from here, not from the origin
    }
  }
  terminal success Done { }
}
```

## See also       {#see-also}
- [ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/) — the schedule the re-based clocks walk (re-resolved on retarget)
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — the `Within` budgets that are re-evaluated
- <span class="planned" title="this page is planned and not written yet">workflow-state</span> — the state `Expire` that is re-evaluated


---

<!-- https://osysharp.com/reference/workflow/run/ -->

# Workflow.Run (start a workflow)

> Start the workflow bound to an entity's type, on that entity. Bare — `Workflow.Run(order)` — is fire-and-forget: start it and carry on. Awaited — `await Workflow.Run("fulfil", order)` — is a durable wait on the started (child) workflow: hold until it reaches a terminal, then return on success or throw `WorkflowError` / `WorkflowCancelled` on an error / cancel terminal, so a parent flow can compensate with an ordinary `try`/`catch`.

<!-- id: workflow-run · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/run/ -->

## Summary        {#summary}
**`Workflow.Run(order)`** starts the workflow whose target binds the entity's **type**, on that entity — the same
inference [Raising a workflow event](https://osysharp.com/reference/workflow/raise/) uses for its events. There are two forms, and the only difference is `await`:

- **`Workflow.Run(order)`** (bare) — **fire-and-forget**: start it and carry straight on. The call returns nothing.
- **`await Workflow.Run("fulfil", order)`** — a **durable wait**: hold here until the started workflow reaches a **terminal**,
  then hand its outcome back. A **success** terminal lets the next line run; an **error** terminal throws
  **`WorkflowError`**; a **cancel** terminal throws **`WorkflowCancelled`**. That makes a child workflow a step you can
  wrap in an ordinary `try`/`catch` and compensate — the Saga pattern.

`await` carries meaning **only** on `Workflow.Run` — it is the one place in Osy# where you wait. Everywhere else effects
run in place, so `await` is never written.

## Signature      {#signature}
```osy syntax
Workflow.Run(order)          // fire-and-forget — start it, carry on; returns nothing
await Workflow.Run("fulfil", order)    // wait for the started workflow's terminal, then return / throw on its outcome
```

`entity` is an entity-typed value whose type has exactly one workflow bound to it in the same unit. More than one is a
compile error (the target is ambiguous); none is a compile error (nothing to start).

## Description    {#description}
A workflow is bound to an entity type (`Tracks = <Entity>.<Enum>;`). `Workflow.Run(order)` starts that workflow on the
given `order` row. Use the **bare** form when the started workflow runs independently — you don't need its result:

```osy title="the bare form — nothing to wait for" syntax
var welcome = new WelcomeEmail { Customer = this.Item };
Workflow.Run(welcome);        // kick it off; this flow carries on
```

Use the **awaited** form when the started workflow is a **step** whose outcome you act on — the essence of a Saga. The
started (child) workflow's terminal surfaces at the `await`:

- a **`success`** terminal → the `await` returns and the next line runs;
- an **`error`** terminal → the `await` throws **`WorkflowError`**;
- a **`cancel`** terminal → the `await` throws **`WorkflowCancelled`**.

Both faults carry the terminal state's message, and both are ordinary catchable exceptions — so a compensating flow is
just `try`/`catch`:

```osy title="the awaited form: catch the fault and compensate" syntax
try {
  await Workflow.Run("payment", payment);        // hold until the payment workflow reaches a terminal
  goto Confirmed;                      // reached only if it SUCCEEDED
} catch (WorkflowError e) {
  await Workflow.Run("refundHold", new RefundHold { Order = this.Item });   // compensate — undo the earlier step
  goto Refunded;
}
```

`WorkflowError` (an error terminal) and `WorkflowCancelled` (a deliberate cancel) are distinct on purpose: catch them
separately when a cancel is not a failure. A `catch (Exception e)` still catches either.

## Examples       {#examples}
```osy title="starting a second workflow from a state" test app=workflow-run
enum OrderState { Placed, Done }

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

entity Order {
  [Required, MaxLength(60)] string Reference;
  OrderState Status;                       // no default: the workflow owns this field
  security { allow read, create, update when IsAuthenticated; }
}

enum NoticeState { Sending, Sent }

entity ShipmentNotice {
  [Required] Order Order;
  NoticeState Status;
  security { allow read, create, update when IsAuthenticated; }
}

workflow NoticeFlow {
  Tracks    = ShipmentNotice.Status;
  Autostart = true;
  Initial   = Sending;
  state Sending { on Complete { goto Sent; } }
  terminal success Sent { }
}

workflow OrderFlow {
  Tracks    = Order.Status;
  Autostart = true;
  Initial   = Placed;

  event Ship();

  state Placed {
    subscribe Ship();
    on Ship {
      // Fire-and-forget: the notice runs on its own, with its own durability.
      // The argument is an entity-typed VARIABLE — `Workflow.Run(new …)` is refused.
      var notice = new ShipmentNotice { Order = this.Item };
      Workflow.Run(notice);
      goto Done;
    }
  }
  terminal success Done { }
}
```

Fire-and-forget — start a notification workflow and move on:
```osy title="fire-and-forget: start a notice and carry on" syntax
var notice = new ShipmentNotice { Order = this.Item };
Workflow.Run(notice);
```

Awaited step with compensation — the Saga shape:
```osy title="the saga shape — a route per terminal outcome" syntax
try {
  await Workflow.Run("reservation", reservation);     // a child workflow; wait for its terminal
} catch (WorkflowError e) {
  goto Rejected;                       // it failed — route accordingly
} catch (WorkflowCancelled e) {
  goto Cancelled;                      // it was cancelled — a different route
}
goto Reserved;                         // it succeeded
```

## Notes          {#notes}
- **`await` is the wait, and the only wait.** `Workflow.Run` without `await` never blocks; with `await` it holds until
  the started workflow terminates. Writing `await` on anything else is a compile error.
- The started workflow is inferred from the entity's **type**, exactly like [Raising a workflow event](https://osysharp.com/reference/workflow/raise/). Keep one workflow per
  bound type, or the target is ambiguous.
- **The wait is durable.** When the started workflow waits — on a human step, a timer, or its own child — the awaiting
  flow **parks**: it is persisted and lifted off the thread, then **resumes** exactly where it paused when the child
  reaches a terminal, even across a restart. You write straight-line `await`; the platform owns the pause. Because it
  parks, the compensating `await` inside a `catch` (or a `finally`) works too — the whole `try`/`catch` survives the
  wait.

## See also       {#see-also}
- [Workflows that outlive the code that started them](https://osysharp.com/reference/workflow/change-over-time/) — what happens to a run parked on an awaited child when you deploy past it
- [Raising a workflow event](https://osysharp.com/reference/workflow/raise/) — send a typed event to a running workflow.
- <span class="planned" title="this page is planned and not written yet">workflow-goto</span> — transition within a workflow body.
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — read a workflow's timeline of events.
- [Step labels (naming a child run so it survives a new version)](https://osysharp.com/reference/workflow/park-label/) — naming an awaited child run so a parked run survives a new version.


---

<!-- https://osysharp.com/reference/workflow/work/ -->

# Workflow.Work&lt;T&gt; (everything outstanding) and its SLA numbers

> Every live slot of every run tracking T, whoever holds it — the unfiltered sibling of Workflow.Inbox. Rows carry Budget, Elapsed and Remaining for the deadline that governs them, so a screen can say "1h32m of the 4h for first response". One row type serves every viewpoint: an operator reads it whole, a requester filters to their own items, a holder filters on Assignee.

<!-- id: workflow-work · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/work/ -->

## Summary        {#summary}
[`Workflow.Inbox<T>()`](https://osysharp.com/reference/workflow/inbox/) answers *what is waiting for me*. **`Workflow.Work<T>()`** answers *what is
outstanding* — every live slot of every run tracking `T`, whoever holds it.

They return the same row, and that is the point: an operator, the person who raised the item, and the pool that can
pick it up all want the same facts, filtered differently.

## Signature      {#signature}
```osy syntax
Workflow.Work<TrackedEntity>()
```
No arguments. Filter, order and count it like any other list.

## Description    {#description}

### How do I get MY queue out of it?   {#viewpoints}
The inbox has a viewpoint built in — assigned to you, or you satisfy the slot's
[`Candidates`](https://osysharp.com/reference/workflow/candidates/). That is what stops it generalising: *"where has my expense report got to"* is
asked by someone who often cannot act on it at all. So the primitive is the unfiltered list, and each viewpoint is an
ordinary `.Where(…)`:

```osy syntax
Workflow.Work<Expense>()                                        // an operations board — everything
Workflow.Work<Expense>().Where(r => r.Assignee == me)           // what I am holding
Workflow.Work<Expense>().Where(r => r.SlaKind == SlaKind.Assigned && r.Remaining < TimeSpan.Zero)  // late to respond

Workflow.Work<Expense>()                                        // my submissions, wherever they are
  .Include(r => r.Item).Include(r => r.Item.Requester)          // …filtering THROUGH Item needs it loaded
  .Where(r => r.Item.Requester == me)
```

⚠ **Filtering through `Item` needs `Item` [included](https://osysharp.com/reference/query/include/).** The rows are already materialised, so a
`.Where(…)` over them runs in memory — an un-included reference has nothing to resolve. Include the hop you filter
on *and* `Item` itself. Fields on the row (`Assignee`, `SlaKind`, `Remaining`) need nothing.

**Security is not what changed.** Dropping the inbox filter drops a *relevance* question, not a permission one:
whether you may see an item at all is decided by that entity's own declared read rules, on the same rows, either way.
A requester who can only read their own expenses sees only their own — with no filter written.

### Which SLA numbers does a row carry?   {#sla}
| member | what it is |
|---|---|
| `Budget` | the SLA's total allowance — the *4h* |
| `Elapsed` | how much is gone — the *1h32m* |
| `Remaining` | `Budget - Elapsed`, **negative** once breached |
| `SlaKind` | which deadline these describe — `Assigned` (first response) or `Finished` (completion) |
| `BreachesAt` | when it runs out |

```osy syntax
foreach (var r in Workflow.Work<Ticket>().OrderBy(r => r.Remaining)) {
  Log.Information($"{r.Item.Title}: {r.Elapsed} of {r.Budget} ({r.SlaKind})");
}
```

**`SlaKind` is not decoration.** A slot can carry both an `Assigned` and a `Finished`
[milestone](https://osysharp.com/reference/workflow/milestone/) — *pick it up within 4h* and *close it within 24h* are different promises. The row
describes the one that **breaches soonest**, which is the same clock `BreachesAt` reports, so a row is always about
one deadline rather than a blend of two. Without `SlaKind`, "1h32m of 4h" would not say which promise it measures.

**`Remaining` goes negative on purpose.** *How far past* is the thing an operator is looking for, and clamping at
zero would flatten the worst rows into the merely-due ones.

**All five are null together** when no clock governs the slot — absence, not a zero that would sort as though the
budget were spent.

### Elapsed is accrued, not wall-clock   {#accrual}
Under [`ServiceHours`](https://osysharp.com/reference/workflow/service-hours/) an SLA only advances during business hours. `Elapsed` counts the same
way, so a ticket raised on Friday afternoon does not burn its budget over the weekend — and `Elapsed`, `Remaining`
and `BreachesAt` on one row always agree with each other. A clock declared `Accrues = false` measures real time, and
its `Elapsed` follows it.

This is also why the numbers come from here rather than being computed in app code: the answer depends on the
schedule the run is governed by, which is not a subtraction anyone can do from the outside.

## Examples       {#examples}
A support queue with a 4h first-response SLA and a 24h close, and the two reads an operations screen makes of it.

```osy title="an operations board, worst first" test app=workflow-work
enum TicketStage { Open, Working, Closed }

[Principal] entity Agent {
  [Required] [MaxLength(80)] string DisplayName;
  [Required] [MaxLength(40)] string Team;
  security {
    allow read   when IsAuthenticated;
    allow create when IsAuthenticated;
  }
}

entity Ticket {
  [Required] [MaxLength(120)] string Title;
  [Required] Agent Reporter;
  TicketStage Stage;
  security {
    allow read, update when IsAuthenticated;
    allow create       when IsAuthenticated;
  }
}

workflow TicketFlow {
  Tracks    = Ticket.Stage;
  Autostart = true;
  Initial   = Open;

  event Pick();
  event Resolve(bool fixed);

  state Open {
    subscribe Pick();
    on Pick { goto Working; }
  }

  state Working {
    subscribe Resolve(bool fixed) as Support {
      Candidates = u => u.Team == "Support";
      Assigned { Within = TimeSpan.FromHours(4);  }   // first response
      Finished { Within = TimeSpan.FromHours(24); }   // close
    }
    on Support(bool fixed) { goto Closed; }
  }

  terminal success Closed { }
}

// Everything past its deadline, worst first — the operator's screen.
int OverdueCount() {
  return Workflow.Work<Ticket>()
                 .Where(r => r.Remaining < TimeSpan.Zero)
                 .Count();
}

// The same read, one viewpoint narrower: where my own tickets have got to. Both Includes are needed — the filter
// navigates Item AND Item.Reporter, and an in-memory Where cannot resolve a reference that was never loaded.
int MySubmissions(Agent me) {
  return Workflow.Work<Ticket>()
                 .Include(r => r.Item)
                 .Include(r => r.Item.Reporter)
                 .Where(r => r.Item.Reporter == me)
                 .Count();
}
```

## See also       {#see-also}
- [Workflow.WorkByItem&lt;T&gt; (one row per item — the board read)](https://osysharp.com/reference/workflow/work-by-item/) — one row per ITEM rather than per slot: the BOARD read, with the clock that governs across an item's slots
- [Workflow.Inbox&lt;T&gt; (what is waiting for me)](https://osysharp.com/reference/workflow/inbox/) — the same row, filtered to the asking principal
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — an operations board shows everyone's work, so use `<Wf>.For(r.Item).<Slot>.Candidates(u)`
  to decide which rows the VIEWER can act on
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — where `Assigned` / `Finished` budgets are declared
- [ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/) — what makes `Elapsed` business hours
- [Workflow.Retarget (re-base the SLA clocks)](https://osysharp.com/reference/workflow/retarget/) — changing an SLA budget mid-run


---

<!-- https://osysharp.com/reference/workflow/work-by-item/ -->

# Workflow.WorkByItem&lt;T&gt; (one row per item — the board read)

> One row per tracked ENTITY, where `Workflow.Work<T>()` is one per SLOT. A board, a queue and a "my work" screen are all per item: an item with three open waits is one card, and a board built on the slot read shows it three times. The row carries the clock that governs across all of an item's slots, who holds it, how many waits it has, and whether any promise has ever been missed.

<!-- id: workflow-work-by-item · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/work-by-item/ -->

## Summary        {#summary}
**`Workflow.WorkByItem<T>()`** returns **one row per tracked entity** that has a live run.
[`Workflow.Work<T>()`](https://osysharp.com/reference/workflow/work/) returns one row per **slot**, and the difference is the difference between a
queue and a **board**.

An item usually waits on more than one thing at once. A ticket being worked might have a fix slot, a park slot and an
escalation slot all open — so a board built straight on the slot read shows that ticket **three times**. Worse, the
duplication passes every "are the tickets there" check you would think to write.

## Signature      {#signature}
```osy title="the call — no arguments, one row per item" syntax
Workflow.WorkByItem<Ticket>()      // List<Osysharp.WorkItemRow_Ticket>
```

Takes no arguments and filters like any query:

```osy title="filtered and ordered like any query" syntax
Workflow.WorkByItem<Ticket>().Where(r => r.EverBreached).OrderBy(r => r.BreachesAt)
```

## Description    {#description}

### The row   {#row}
| member | what it is |
|---|---|
| `Item` | the tracked entity, typed — navigable and `.Include`-able |
| `Assignee` | who holds the item, **resolved across all its slots** |
| `OpenSlots` | how many live waits it has |
| `EverBreached` | has any promise on this run ever been missed |
| `SlaKind` · `Budget` · `Elapsed` · `Remaining` · `BreachesAt` | the **governing** clock across the item's slots |
| `GoverningSlot` | which slot PROMISED those numbers — **null when the run's own clock governs** (see below) |
| `RunId` · `WorkflowName` | the run driving it |

### The holder is resolved across ALL slots    {#holder}
A card asks *"who has this ticket"*, and **any** slot somebody holds answers it — the person who took the reply slot
is working the ticket whether or not that slot is the one breaching soonest.

This is worth stating because the obvious shortcut is wrong in a way that is hard to see. Slots of one run routinely
**share a breach instant** — a state's `Expire` governs several of them, each computed separately — so "the governing
slot" is not a stable choice between them. An app that read the holder off the governing row got a card saying
*"unassigned"* while its own roster said the ticket was held.

### The governing clock, and how ties break    {#governing}
The SLA members describe the promise breaching **soonest** across the item's slots. Ties are the normal case, not an
edge one, so the rule is fixed and stated rather than left to each app to discover:

1. soonest `BreachesAt`;
2. then a slot whose clock is **its own** rather than the run's;
3. then a slot that somebody **holds**;
4. then the slot's **declaration order**.

### A slot with no promise of its own borrows the run's    {#borrowed}
A slot that declares no milestone still reports an SLA — the **run's**: the state's `Expire`, or the instance
`Deadline`. That is deliberate, and it is why those two `SlaKind` members exist: a ticket waiting in a state whose
promise is ticking should say so, not report nothing.

⚠ **But a borrowed clock is not that slot's promise, and every promise-less slot on a run borrows the SAME one.**
So rule 2 exists: choosing between them by an instant they all share is choosing arbitrarily, and before it existed
the arbiter was declaration order — which a workflow-scope [`subscribe`](https://osysharp.com/reference/workflow/subscribe/) wins forever, because
it is declared before any state's slots. An always-open escalation hatch became what the board counted down, what
"take it" claimed, and what every verb on the page was aimed at, while the reply the item was actually waiting for
sat untouched. Nothing errored anywhere.

### `GoverningSlot` is null when the run's own clock governs    {#governing-slot-null}
When the winning clock is the run's, **no slot is named** — because none promised it. Naming one would say "this
countdown is counting `Bump`" when it is counting the state's expiry, which `Bump` merely stands next to.
`SlaKind` says which run promise it is, so a screen can still say what it is counting.

⚑ **This is also the difference between a wrong answer and a visible one.** An app reading `GoverningSlot` as a
claim target now gets a null it has to handle, rather than a slot the caller cannot claim and a refusal two steps
later. Claiming is a different question — *"which row may I take?"* — and wants [`Workflow.Work<T>()`](https://osysharp.com/reference/workflow/work/),
which still reports the borrowed clock per slot exactly as before.

### An item with no live clock still appears    {#no-clock}
A parked, blocked or finished item is still on the board. It comes back with every SLA member **null** — absence, not
a zero, which would sort as a fully-consumed budget and put the calmest rows where the worst ones belong.

### `EverBreached` survives the clock being retired    {#ever-breached}
⚑ **This is the member that makes a board correct, and the reason is not obvious.** When a promise runs out the engine
stamps the breach and **retires the clock** — so the item stops having a countdown at all. A board ordered by "has a
live promise" therefore drops the one row everybody needs to see, at the exact moment it starts mattering, and sorts
breached work to the bottom.

`EverBreached` is read from the workflow's own [audit trail](https://osysharp.com/reference/workflow/audit/), so it is still true afterwards. An app
does not need to keep its own flag for this.

#### …but it does not survive the RUN ending    {#ever-breached-run-ended}
⚠ **The clock and the run are two different horizons, and this member only outlives the first.** `WorkByItem` returns
one row per **live** run — so when a breach arm ends the run (`Unfinished { goto Expired; }`, the ordinary shape for a
deadline that actually means something) the row disappears, and `EverBreached` goes with it. The board asks *"was this
ever late?"* in the same sweep that made the answer true, and gets nothing back at all.

That is not a bug in the read: a finished item has no live work, which is exactly what this read is for. It is a
reason to ask a different source. **The [audit trail](https://osysharp.com/reference/workflow/audit/) has no such horizon** — it is where
`EverBreached` came from in the first place, and it is still there when the run is over:

```osy syntax
foreach (var a in Onboarding.For(i).Audit) {
  if (a.Kind == AuditKind.Breached) { everBreached = true; }
  if (a.Kind == AuditKind.Reminded) { nudges = nudges + 1; }
}
```

So: read `EverBreached` off the row when the breach **leaves the run open** (a support ticket still owed an answer —
the case this member was built for), and off the trail when the breach **ends it**. A board that mixes finished and
unfinished items wants the trail, because only it answers for both.

## Examples       {#examples}
A whole desk, compiled: three waits open on one ticket, and a board that shows it once.

```osy title="a board over a multi-wait item" test app=workflow-work-by-item
enum Stage { Working, Done }

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

entity Ticket {
  [Required, MaxLength(120)] string Subject;
  Stage State;
  security { allow read, create, update when IsAuthenticated; }
}

workflow TicketFlow {
  Tracks    = Ticket.State;
  Autostart = true;
  Initial   = Working;

  event Fix();
  event Park();
  event Escalate();

  // Three things can happen to a ticket being worked. On a SLOT board that is three rows; here it is one card
  // that knows it is waiting on three things.
  state Working {
    subscribe Fix()      as FixIt  { }
    subscribe Park()     as Parked { }
    subscribe Escalate() as Raised { }
    on Complete { goto Done; }
  }

  terminal success Done { }
}

// The board: one row per ticket, worst first. `EverBreached` leads because a breach retires the clock, so ordering
// on the live figures alone sends the worst row to the bottom.
List<Osysharp.WorkItemRow_Ticket> Board() {
  return Workflow.WorkByItem<Ticket>()
    .Include(r => r.Item)
    .OrderBy(r => r.EverBreached ? 0 : 1)
    .ThenBy(r => r.BreachesAt)
    .ToList();
}

// "Waiting on more than one thing" — a question the slot board cannot ask at all.
int Stalled() { return Workflow.WorkByItem<Ticket>().Where(r => r.OpenSlots > 1).Count(); }
```

Two more questions the same row answers. "What am I working on":

```osy title="what am I working on" syntax
Workflow.WorkByItem<Ticket>().Where(r => r.Assignee == Session.CurrentUser.Id)
```

"Waiting on more than one thing":

```osy title="waiting on more than one thing" syntax
Workflow.WorkByItem<Ticket>().Where(r => r.OpenSlots > 1)
```

## See also       {#see-also}
- [Workflow.Work&lt;T&gt; (everything outstanding) and its SLA numbers](https://osysharp.com/reference/workflow/work/) — one row per SLOT: the queue read, and where the SLA numbers are explained in full
- [Workflow.Inbox&lt;T&gt; (what is waiting for me)](https://osysharp.com/reference/workflow/inbox/) — the same read filtered to the current principal's own work
- [Transitions — where this item may go next](https://osysharp.com/reference/workflow/transitions/) — the lanes a card on this board may be dropped into
- [ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/) — why `Elapsed` is accrued rather than wall-clock


---

<!-- https://osysharp.com/reference/workflow/index/ -->

# Workflows (the run that outlives the request)

> A `workflow` is a run that survives the process it started in — it parks, waits days for a person or a timer, and resumes in a later deployment. The first-day mistake is writing one like a long function: every `await` is a place the app may be redeployed underneath you, so each awaited step carries a label you choose, and any call that leaves the platform is already a durable step you neither mark nor can forget.

<!-- id: workflow-index · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/index/ -->

## Summary        {#summary}
**A workflow is a run that outlives the request that started it.** It parks — on a person, on a timer, on a child
run — and resumes later, possibly days later, possibly in a deployment that did not exist when it began. Everything
below follows from that one fact.

```osy title="the shape: what it tracks, what leaves each state, and what the clock does" test app=workflow-index
enum InvoiceStatus { Submitted, Approved, Escalated }

entity Invoice {
  [Required, MaxLength(60)] string Reference;
  decimal Amount;
  InvoiceStatus Status = InvoiceStatus.Submitted;
}

workflow Approval {
  Tracks = Invoice.Status;
  Initial = Submitted;

  event Approve();

  state Submitted {
    Expire = TimeSpan.FromDays(2);    // nobody acted → the clock acts

    subscribe Approve();              // a person acts → a transition
    on Approve { goto Approved; }
    on Expire  { goto Escalated; }
  }
  terminal success Approved { }
  terminal cancel  Escalated { }
}
```

## Description    {#description}
Three properties separate a workflow from a long function, and each is the source of a different first-day mistake.

**It parks, so a step needs a name.** Awaiting a child run suspends this one. While it is suspended the app can be
recompiled and redeployed, and the resuming version has to recognise which step the run is sitting at — so every
awaited step carries a **label** you write, a literal string, unique within the workflow. See
[Step labels (naming a child run so it survives a new version)](https://osysharp.com/reference/workflow/park-label/). A fire-and-forget start never parks and needs none.

**It replays, so leaving the platform is special.** On resume the engine re-executes the body up to where the run
had reached. A call that already went out must not go out twice — a charge, an email, a webhook. You do not mark
those: the compiler makes every outbound call a durable step and replays its recorded answer instead of repeating
it ([Automatic durability (steps you do not have to write)](https://osysharp.com/reference/workflow/automatic-durability/)). What you must not do is reach for a clock or a random number as if they
were ordinary — see [Wall-time clocks (Accrues = false)](https://osysharp.com/reference/workflow/wall-time-clock/).

**`Autostart` decides whether a run begins on its own, and WHEN it actually runs is exact, not a loose "soon":** an
`Autostart = true` run's initial `enter{}` executes synchronously, inside the SAME commit that created the tracked
row — see [[workflow-tracks#autostart-timing]] for the guarantee and what it means for `Workflow.Settle`.

**It is a state machine, so what may happen next is data.** `Workflow.Transitions(item)` answers the moves this
instance can make *right now*, each arm's guard already evaluated ([Transitions — where this item may go next](https://osysharp.com/reference/workflow/transitions/)) — a board offers the
lanes a card can actually reach instead of accepting a drop and having the engine refuse it afterwards.
`Workflow.Raise(item, move)` takes one of them by handing the row back.

**Who holds the work is also data.** [Workflow.Inbox&lt;T&gt; (what is waiting for me)](https://osysharp.com/reference/workflow/inbox/) is what *this principal* may act on; [Workflow.Work&lt;T&gt; (everything outstanding) and its SLA numbers](https://osysharp.com/reference/workflow/work/) is
every live slot of every run, whoever holds it, with the deadline budget attached. They are the same rows read from
two viewpoints, and picking the wrong one is how an operator screen quietly becomes a personal one.

## The pages      {#the-pages}
Run `osy docs workflow` for the full listing. The groups:

- **Declaring one** — [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/), [Tracks and Initial (the field a workflow drives)](https://osysharp.com/reference/workflow/tracks/), [enter and exit (a state's arrival and departure hooks)](https://osysharp.com/reference/workflow/enter-exit/), [Transitions — where this item may go next](https://osysharp.com/reference/workflow/transitions/),
  [Requires — named preconditions, and the live checklist](https://osysharp.com/reference/workflow/requires/), [[Authorize] (event)](https://osysharp.com/reference/workflow/authorize/), [What a workflow body may write](https://osysharp.com/reference/workflow/body-security/)
- **Waiting** — [subscribe](https://osysharp.com/reference/workflow/subscribe/), [Remind (milestone reminders)](https://osysharp.com/reference/workflow/remind/), [Workflow.Once (run a step at most once)](https://osysharp.com/reference/workflow/once/), [ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/),
  [Wall-time clocks (Accrues = false)](https://osysharp.com/reference/workflow/wall-time-clock/), [Backoff (retry policy)](https://osysharp.com/reference/workflow/backoff/)
- **Branching and joining** — [fan-out (foreach subscribe)](https://osysharp.com/reference/workflow/fan-out/), [dynamic fan-out (foreach over a runtime collection)](https://osysharp.com/reference/workflow/fan-out-dynamic/), [Parallel legs (start several, then wait for them)](https://osysharp.com/reference/workflow/parallel-legs/),
  [complete when (a state's own completion condition)](https://osysharp.com/reference/workflow/complete-when/), [slot dependencies (After / When / Pending)](https://osysharp.com/reference/workflow/slot-dependencies/)
- **Work and people** — [Workflow.Inbox&lt;T&gt; (what is waiting for me)](https://osysharp.com/reference/workflow/inbox/), [Acting on an inbox row (deposit, claim, release)](https://osysharp.com/reference/workflow/inbox-act/), [Workflow.Work&lt;T&gt; (everything outstanding) and its SLA numbers](https://osysharp.com/reference/workflow/work/), [Workflow.WorkByItem&lt;T&gt; (one row per item — the board read)](https://osysharp.com/reference/workflow/work-by-item/),
  [Assign — handing a slot to a named colleague](https://osysharp.com/reference/workflow/assign/), [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/), [actor — who just did this](https://osysharp.com/reference/workflow/actor/), [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/)
- **Surviving deployment** — [Step labels (naming a child run so it survives a new version)](https://osysharp.com/reference/workflow/park-label/), [Migrating runs that are still in flight](https://osysharp.com/reference/workflow/migration/), [Workflow.Retarget (re-base the SLA clocks)](https://osysharp.com/reference/workflow/retarget/),
  [Workflows that outlive the code that started them](https://osysharp.com/reference/workflow/change-over-time/), [Automatic durability (steps you do not have to write)](https://osysharp.com/reference/workflow/automatic-durability/)
- **Watching it** — [For(entity).Audit](https://osysharp.com/reference/workflow/audit/), [Flow metrics — how long an item took, and how much was waiting](https://osysharp.com/reference/workflow/flow-metrics/), [When a child is cancelled or fails](https://osysharp.com/reference/workflow/fault-propagation/)

## See also   {#see-also}
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — starting one, and the difference between awaiting and firing and forgetting
- [Automatic durability (steps you do not have to write)](https://osysharp.com/reference/workflow/automatic-durability/) — why you never mark an outbound call
- [Step labels (naming a child run so it survives a new version)](https://osysharp.com/reference/workflow/park-label/) — the one thing you must write by hand for a run to survive a deploy


---

<!-- https://osysharp.com/reference/workflow/change-over-time/ -->

# Workflows that outlive the code that started them

> A workflow run can be waiting for days while you deploy past it a dozen times. This is how the platform decides where such a run IS in terms the new code understands, what moves automatically, what you have to say out loud, and what it refuses to guess. Read it before your first migration, or to judge whether this engine can carry work across the changes a real system makes.

<!-- id: workflow-change-over-time · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/change-over-time/ -->

## Summary        {#summary}
A workflow run is not a request. It can sit waiting for an approval on Friday and be resumed on Tuesday, and in
between you will have deployed. The code that finishes the run is not the code that started it.

Every durable workflow engine has to answer one question to make that safe: **where is this run, said in terms the
new code can understand?** This page is that answer — the identity model underneath it, what the platform moves for
you, what it asks you to decide, and what it refuses to guess.

## Description    {#description}

### What is a run's position, and why can a deploy move it?   {#the-problem}
A run's position is more than "which state". Halfway through a body it is also: which step already ran, which child
run belongs to which step, which of several `foreach` items it is on, and what its local variables held. All of that
was recorded by one version of your code, and has to be read by the next one.

The failure mode is specific and expensive. If a position is recorded as *where it sat* — the twelfth node of a tree,
the third call in a body — then editing anything above it moves it. A run resuming after that deploy looks for work
it has already done, does not find it, and **does it again**. For a charge or a message, "again" is the whole problem
the engine exists to prevent, and nothing announces it.

### The principle: a position is made of names you chose   {#names}
Every part of a run's position is a name that appears in your source, so that editing the code around it cannot move
it. Nothing in the list below is a number, an index, or an id minted by a compile:

| what has to be identified | what identifies it |
|---|---|
| which state the run is in | the state's name |
| which body it is running | where that body is declared — `Approved.enter`; for one arm of a route, the **guard you wrote** |
| which call it is inside | the callee's name |
| which park point it is sitting at | the label you wrote — `await Workflow.Run("fulfil", order)` |
| which durable step already ran | that step's label — `Workflow.Once("charge", …)` |
| which child run belongs to that step | the starting run, plus the step's label |
| an external call you did not label | the call itself — `Resend.Send` |
| which pass of a loop | the iteration number |

The last row is the exception that proves the rule: an iteration says *which pass over the data*, not which line of
code. There is nothing in your source to name, so a number is the honest identity.

**A `when` arm has no name, so its guard is its name.** You never write a label on an arm, and it would be ceremony
to ask for one — so the arm a run is parked in is identified by the condition you wrote on it. Everything follows
from that, in both directions:

```osy syntax
on Decide {
  when (this.Item.Amount > 1000) { … }     // identified as this condition,
  when (this.Item.Amount > 100)  { … }     // and this one — not as "the first" and "the second"
}
```

- **Reorder the arms, or add one above them, and nothing moves.** A run parked in the `> 1000` arm resumes in the
  `> 1000` arm. Had the arm been "the first one", inserting a new arm above it would have quietly made a parked run
  resume in a branch it had never been in — carrying the memos of the branch it *was* in, so the steps it already
  ran would be treated as done.
- **Change a guard and you have changed which arm that is**, so a run parked in it needs your word. It is reported
  the same way as a renamed body: the new version has no arm of that description, and the migration asks you to say
  where the run should go.

### Where a name is not enough, the compiler says so — before a run exists   {#refusals}
A name identifies something only while it is unique in its scope. Two steps labelled `"charge"` in one body, two
unlabelled calls to `Resend.Send`, two calls to one durable helper, **two arms of one route under the same guard,
two `default` arms** — each is a position with no answer.

**Those are compile errors, and *when* they fire is the entire point.** The alternative is to notice at deploy time,
against a run that is already parked — where adding a label cannot help *that* run, because the label was not there
when it parked. That is a refusal nobody can act on, firing on every deploy forever. Caught while you are writing the
code, each one costs a single string literal.

This is why the language asks for a few labels it could have derived. A derived default is not a name you chose:
rename a local, or a workflow, and it changes silently, breaking the migration of every run already parked there.

### Two kinds of waiting, two behaviours   {#two-kinds}
Where a run is waiting decides how a deploy treats it.

**Waiting at a state** — for an event, an approval, a timer. This is most runs most of the time. The run holds no
call stack; its whole position is "in state X, with these slots open". A deploy **moves it automatically**: the state,
each slot, and each live clock are re-pointed at the new version's declarations by matching names.

**Parked mid-body** — inside `await Workflow.Run(…)`, `await saga.Run(…)`, or a leg join, with a live call stack and
local variables. **By default this run stays on the version it started under** and finishes with that version's
behaviour.

That default is a decision, not an omission. "Runs started under v1 keep v1's behaviour; new ones get v2" is often
exactly right, and always the safer reading of a deploy. When you want in-flight work to pick up the new body, say so
per state:

```osy syntax
migration OrderSaga v2 -> v3 {
  on Fulfilling { keep; reenter; }     // `keep` = stays in Fulfilling; `reenter` = and re-enters the new body
}
```

`reenter` is orthogonal to position: `keep` / `goto` / `terminate` say **where** the run lands, `reenter` says **how**
it gets there. You still have to say where.

### What re-entry actually does   {#re-entry}
There is no cursor to translate — a cursor is a position in one version's tree. Instead, **the new body runs from the
top**, and the work already done is recognised where it is recorded:

- a `Workflow.Once` step finds its memo and returns the recorded result without running;
- an `await Workflow.Run` / `saga.Run` finds its child by *(starting run, step label)*, and if that child has
  finished, takes its outcome and carries on.

So execution passes straight through everything already satisfied and stops at the first thing that has not happened.

**Completed steps are re-executed, not skipped**, and the difference matters: running a finished `saga.Run` is what
puts its compensation back on the undo stack. A re-entry that jumped over it would leave step 1 uncompensated when
step 4 fails.

Two consequences to design for:

- **Ordinary statements between the steps run again.** Assignments are idempotent, so they are fine. Creating a row
  is not — so creating one *before* a wait is a compile error, and the fix is to put the creation in the step that
  already records its result:
  Work after the last wait is reached only once and needs nothing.
- **Values from `DurableClock.Now` or `Guid.NewGuid` are derived afresh.** Those ride the parked cursor, which is the thing
  a version move discards. Anything that must not change is a durable step result, which is recorded out of band
  against a stable id and survives.

The replay-safe shape, in full — every line that must not happen twice is recorded:

```osy title="a body that can be re-entered safely" test app=wf-change-over-time
enum OrderStatus { Placing, Placed, Failed }
enum StepStatus  { Waiting, Done }

entity Order { [Required, MaxLength(20)] string Ref; OrderStatus Status = OrderStatus.Placing; }
entity Charge { [Required] Order Order; StepStatus Status = StepStatus.Waiting; }

int Bill(Order o) { return 1; }

workflow ChargeFlow {
  Tracks = Charge.Status; Autostart = false; Initial = Waiting;
  event Settled();
  state Waiting { subscribe Settled(); on Settled { goto Done; } }
  terminal success Done { }
}

workflow PlaceOrder {
  Tracks = Order.Status; Autostart = false; Initial = Placing;
  state Placing {
    enter {
      // Recorded, so a re-entry returns the same result instead of billing again.
      Workflow.Once("bill", () => Bill(this.Item));
      // Recorded too — otherwise a re-entry would create a SECOND Charge row. This is the compile rule.
      var c = Workflow.Once("make-charge", () => new Charge { Order = this.Item });
      // The park. On re-entry this finds the child it already started and takes its outcome.
      await Workflow.Run("charge", c);
      goto Placed;
    }
  }
  terminal success Placed { }
  terminal error   Failed { Message = "could not place the order"; }
}
```

### What is refused, and why each refusal is honest   {#refused}
A refusal leaves the run untouched on its old version, still working. It is a normal outcome of a deploy, not a
failure of one — and each names the specific thing that could not be matched, never "this run is complicated".

| refused | why | what to do |
|---|---|---|
| a state the new version does not have | a rename and a removal look identical from outside | say which with `goto` or `terminate` |
| a slot that vanished | the run is holding a claim on it | `rename slot A -> B;` or `drop slot A;` |
| a deadline whose budget moved | carrying breaches every live SLA at once; resetting hides breaches that happened | `carry clock` or `reset clock` — there is no safe default |
| a fan-out whose shape changed | the run holds deposits against the old shape | let those runs finish |
| a mid-body park, with no `reenter` | the default: it keeps the behaviour it started with | add `reenter;`, or let it finish, or cancel it |
| a mid-body park **inside a loop** | its item list and cursor are not describable as "which steps completed" | let it finish |
| a body the new version has no counterpart for | a renamed body is a rename only you can confirm | `goto`, or let it finish |

### Several deploys while a run waits   {#hops}
A run moving v1→v3 runs v1→v2's migration and then v2→v3's, in order — never a single composed jump. The verbs are
cumulative and order-dependent: a `goto` in the first hop decides which state the second hop's verbs even apply to.
Composing the endpoints would land the run in a state the first author had deliberately steered it away from.

### Which migration statement do I want?   {#when}
- **You changed a body's logic and want in-flight work to use it** → `reenter;` on that state. Ask first whether work
  already in progress *should* change behaviour mid-flight; often it should not.
- **You renamed a state** → `goto <NewName>;`.
- **You removed a state or an entire branch** → `terminate <outcome> "<why>";`. The message reaches the run's parent.
- **You changed an SLA budget** → `carry clock` (the time already spent counts) or `reset clock` (it is forgiven).
- **A state is unchanged** → `keep;`. Say it anyway: the compile requires every parkable state to be spoken for, so
  that a state you forgot is a build error rather than a stranded run.
- **You are not sure what is out there** → `osy workflow-runs` lists what is still on an old version, and
  `osy migrate --dry-run` decides everything and writes nothing.

### The hard questions about deploys, answered   {#evaluating}
The questions worth asking of any durable workflow engine, and where this one stands:

- **Can a running workflow survive a deploy?** Yes. Runs waiting at a state move automatically; runs parked mid-body
  keep their original behaviour unless you opt them into the new one.
- **What happens when I edit a body a run is inside?** Nothing, unless you ask. Its identity is names, not positions,
  so ordinary edits — inserting a line, reordering unrelated work, renaming a local — do not move it.
- **Is there a class of edit that silently corrupts a running workflow?** The ones that would are compile errors: two
  steps sharing a label, two unlabelled calls to the same external target, two calls to one durable helper, a row
  created before a wait. The design principle is that anything the platform cannot answer is refused while you are
  writing it, not while a run is parked on it.
- **Do I have to reason about determinism?** Only where it is real: durable step results are recorded, so they never
  change; a clock read taken before a park is re-derived after a version move. There is no replay-determinism error
  class to learn, because a run that has not migrated does not replay at all — it resumes from its cursor.
- **Can I test what happens across a deploy?** Yes, and it is first-class rather than a harness trick: `TestClock.Advance`
  moves time, `[runas(P)]` runs as a specific principal, and a test can compile v1, start a run, compile v2 with a
  migration, and assert where the run lands.
- **What is the operational surface?** `osy workflow-runs` (what is still on an old version), `osy migrate --dry-run`
  (what a deploy would do), `osy cancel-runs` (the abort). A deploy drains automatically; versions are retained while
  runs still hold them.

## See also       {#see-also}
- [Migrating runs that are still in flight](https://osysharp.com/reference/workflow/migration/) — the verbs, in full, with the exhaustiveness rule and worked examples
- [Moving runs onto the version you just deployed](https://osysharp.com/reference/project/migrating-runs/) — the operator surface: seeing what is out there, dry runs, draining
- [Step labels (naming a child run so it survives a new version)](https://osysharp.com/reference/workflow/park-label/) — the label that makes a park point findable across versions
- [Workflow.Once (run a step at most once)](https://osysharp.com/reference/workflow/once/) — durable steps, their labels, and the memo a re-entry reads
- [Deploying while workflows are running](https://osysharp.com/reference/project/app-versions/) — what a version is, and which runs are still holding an old one open


---

<!-- https://osysharp.com/reference/workflow/authorize/ -->

# [Authorize] (event)

> Gates WHO may raise a workflow event. `[Authorize]` on an event is a `principal => bool` predicate over the acting principal and `this.Item`; a principal who fails it is refused when they try to raise the event. This is produce-side authorization — a distinct question from a slot's `Candidates` (who may HOLD a work item), and the only way to gate an event that has no slot (e.g. a `Cancel`).

<!-- id: workflow-authorize · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/authorize/ -->

## Summary        {#summary}
`[Authorize]` on a workflow `event` declares WHO may **raise** it. It is a single-parameter `principal => <predicate>`
lambda — the parameter is the acting principal, and `this.Item` (the workflow's tracked entity) is in scope — evaluated
when the event is raised. A principal who does not satisfy the predicate is refused; the raise never runs a route or a
transition. This is **produce-side** authorization: a different question from a slot's `Candidates` (who may HOLD a work
item), and the answer for an event that has no slot to hang authorization on — the canonical case being a `Cancel` that
any state should honour but only certain principals may trigger.

## Signature      {#signature}
```osy syntax
[Authorize(<principal> => <predicate over the principal and this.Item>)]
event <Name>(<typed params>);
```

## Description    {#description}
An event is a thing the outside world can raise on a running workflow. Left undecorated, anyone with access to the
workflow's surface may raise it. `[Authorize]` narrows that: it is a boolean predicate the engine evaluates against the
**acting principal** the moment the event is raised, before any routing or transition happens.

- The lambda takes exactly **one parameter** — the acting principal — typed as the app's `[Principal]` entity.
- `this.Item` (the tracked entity) is in scope, so the predicate can compare the principal to the item — the common
  shape is ownership (`u => u == this.Item.Requester`) or a role/relationship test
  (`u => u == this.Item.Requester || u.Role == Role.Support`).
- The predicate is **fail-closed**: if there is no acting principal, no `[Principal]` entity, or the principal cannot be
  resolved, the raise is refused.

A refused raise **throws** — it does not route to an `on <Event>.Denied` arm (that would invite an author to write an
empty one and silently swallow a security failure). The refusal is recorded on the workflow's audit timeline, so "who
tried to raise this and was refused" is a query, and the entity does not move.

### Relationship to `Candidates`   {#vs-candidates}
`[Authorize]` and a slot's `Candidates` answer different questions and do not substitute for each other:

| | Question | Scope |
|---|---|---|
| `[Authorize]` on an `event` | who may **raise** this event | the event (workflow-wide) |
| `Candidates` on a `subscribe` | who may **hold / satisfy** this slot | one slot in one state |

An event with no slot (`Cancel`) can only be gated with `[Authorize]`. A slot in a specific state that different
principals may act on is gated with `Candidates`. A workflow may use both.

### Raising a gated event from anywhere   {#workflow-route}
A workflow-level route (`on Cancel { goto Cancelled; }`, declared once beside the states) is live in every non-terminal
state, so a gated `Cancel` can be raised at any point in the run and the same route decides where it goes. A state may
override the workflow-level route for that event by declaring its own `on <Event>` (nearest-wins).

## Examples       {#examples}
```osy title="who may raise the event" test app=workflow-authorize
enum Decision   { Approve, Reject }
enum OrderState { Placed, Done }

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

entity Order {
  [Required, MaxLength(60)] string Reference;
  [Required] Person Requester;
  OrderState Status;                       // no default: the workflow owns this field
  security { allow read, create, update when IsAuthenticated; }
}

workflow OrderFlow {
  Tracks    = Order.Status;
  Autostart = true;
  Initial   = Placed;

  // The rule travels with the EVENT, so every path that could raise it is covered by one line.
  [Authorize(u => u == this.Item.Requester)]
  event Cancel();

  state Placed {
    subscribe Cancel();
    on Cancel { goto Done; }
  }
  terminal success Done { }
}
```

Only the requester or a support agent may cancel an order, and a cancellation is honoured from any state:

```osy title="an authorized event with a workflow-level route" syntax app=purchase-approval
[Authorize(u => u == this.Item.Requester || u.Role == Role.Support)]
event Cancel();

on Cancel { goto Cancelled; }
```

The requester alone may cancel:

```osy title="ownership gate" syntax app=purchase-approval
[Authorize(u => u == this.Item.Requester)]
event Cancel();
```

## See also       {#see-also}
- [What a workflow body may write](https://osysharp.com/reference/workflow/body-security/) — what authority the workflow's OWN body writes with (an `enter`, an `on` handler): the
  engine's, not the raiser's — the question every invitation flow asks next, and the one this page does not answer
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — a slot's `Candidates`, the who-may-HOLD gate (contrast with who-may-RAISE here)


---

<!-- https://osysharp.com/reference/workflow/actor/ -->

# actor — who just did this

> Inside any workflow body, `actor` is the principal whose action drove this dispatch — who deposited the event, who raised it, whose write started the run. It is nothing when the engine drove the body itself: a reminder, an SLA breach, a deadline, a callback from someone with no account. It is deliberately NOT the slot's `Assignee`, which answers a different question — whose queue the work sat in — and gives a different answer whenever the two diverge.

<!-- id: workflow-actor · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/actor/ -->

## Summary        {#summary}
`actor` is the ambient principal of a **workflow body**: the person whose action caused this body to run. It is in
scope in a `Start`, a state `enter`/`exit`, a route arm and a milestone body, and it is typed as the app's
`[Principal]` entity.

It answers **who just did this**. That is a different question from [`Assignee`](https://osysharp.com/reference/workflow/assign/), which answers
**whose queue was this in** — and the two give different answers exactly where it matters.

`actor` is **nothing** when the engine drove the body rather than a person. That is a real answer, not a gap.

## Signature      {#signature}
```osy syntax
on <Slot>(<payload>) {
  this.Item.DecidedBy = actor;         // WHO DID IT   — the principal who deposited
  this.Item.Queue     = <Slot>.Assignee;  // WHOSE QUEUE — nothing on a pool slot nobody claimed
}
```

## Description    {#description}

### Two questions, two answers   {#two-questions}
A slot is a **place work waits**. `Assignee` names who is holding that place. `actor` names who performed the act the
body is running for. Most of the time they are the same person and the distinction is invisible — which is exactly why
it is worth stating, because the cases where they differ are the ones anybody ever asks about later:

| situation | `actor` | `<Slot>.Assignee` |
|---|---|---|
| a claimed slot, decided by its holder | the holder | the holder |
| a **pool** slot decided with no claim | the depositor | **nothing** — it was never anybody's |
| a slot **assigned** to one person, acted on by another | the person who acted | the person it was assigned to |
| a reminder, breach, deadline or expiry | **nothing** | whoever holds it, if anyone |
| a route with no slot at all (`on Cancel`) | whoever raised it | there is no slot to ask |

Reading `Assignee` when you meant `actor` is the single commonest way a workflow records the wrong person, and it
fails **silently**: on a pool slot it writes nothing at all, and on an assigned slot it writes a plausible name that is
not the one who acted.

### A pool slot decided without a claim   {#pool-slot}
A pool slot — one with [`Candidates`](https://osysharp.com/reference/workflow/candidates/) and no `Assignee` — belongs to **nobody** until somebody
calls `Claim()`. Depositing its event satisfies the slot without ever assigning it, so `Assignee` is still nothing
inside the arm. `actor` is who deposited it:

```osy title="crediting the decision to who made it" test app=workflow-actor-pool
enum PoStage { Review, Done }
enum Dept { Legal, Finance }

[Principal] entity Person {
  [Required, MaxLength(80)] string Name;
  Dept Department = Dept.Legal;
  security { allow read, create when IsAuthenticated; }
}

entity Po {
  [Required, MaxLength(120)] string Title;
  PoStage Stage;
  Person DecidedBy;
  security { allow read, create, update when IsAuthenticated; }
}

workflow PoApproval {
  Tracks    = Po.Stage;
  Autostart = true;
  Initial   = Review;

  event Approve();

  state Review {
    subscribe Approve() as Legal {
      Candidates = u => u.Department == Dept.Legal;
    }

    // `Legal.Assignee` is nothing here — nobody claimed it. `actor` is who approved.
    on Legal { this.Item.DecidedBy = actor; goto Done; }
  }

  terminal success Done { }
}
```

There is no ceremony to write. You do **not** need to claim the slot on the way in to have somebody to credit, and
claiming in order to record an actor would write down a falsehood: it would record whoever acted as the **assigned
approver**, which on a pool slot they never were.

### A route with no slot   {#no-slot}
An event route that is not keyed to a slot — a cancel, an escalation, anything raised on the run as a whole — has no
slot to ask at all. `actor` is the only thing that can answer, and it does:

```osy title="a cancel names who cancelled it" syntax
on Cancel { this.Item.CancelledBy = actor; goto Cancelled; }
```

### When the engine acts, `actor` is nothing   {#engine}
A body the **engine** drives has no acting principal, and `actor` is nothing there. That covers every clock-driven and
unattended path:

- a [reminder](https://osysharp.com/reference/workflow/remind/) firing;
- a [milestone breach](https://osysharp.com/reference/workflow/milestone/) — `Unassigned`, `Unfinished`, `Exhausted`;
- an `Expire` or `Deadline` elapsing;
- a [callback URL](https://osysharp.com/reference/workflow/callback-url/) redeemed by a third party who has no account.

⚠ **It is nothing, not "the last person who touched this run."** A ticket that Lena submitted and that then breached
its SLA overnight was not breached *by Lena*, and a timeline saying so is worse than one saying nothing. If you need
to distinguish *which* unattended path it was, the [audit trail](https://osysharp.com/reference/workflow/audit/) records the event kind, and a slot
records how it was satisfied.

A body that must behave differently when nobody acted asks directly:

```osy title="a body that runs both ways" syntax
Unassigned {
  if (actor == null) { Escalate(); return; }   // the clock got here first
  Notify(actor);
}
```

### It survives a park   {#replay}
`actor` is a **fact of the dispatch**, recorded when the action happened — not a question about who is signed in now.
A body that parks on an `await` and resumes days later still names the same person. That is why `Session.CurrentUser`
is the wrong question inside a workflow body and is correctly nothing there (see [What a workflow body may write](https://osysharp.com/reference/workflow/body-security/)): a
resuming body runs on the engine's own authority, with nobody signed in to ask about.

A body started by another body — a saga child, a `Workflow.Run` — inherits the actor of the body that started it,
because the person's action is what caused it to exist.

### It is the caller's, and app code cannot set it   {#trustworthy}
The value comes from the authenticated principal at the raising call and travels engine-side only. There is no
assignment form, no argument that carries one, and nothing in the language that can name a different person. So it is
the value to record when the question is *"who approved this"* and somebody will one day need the answer to be true.

`actor` is also what the [audit trail](https://osysharp.com/reference/workflow/audit/) records for the same event, so a hand-written record and the
platform's own timeline cannot disagree.

### Where it is not in scope   {#not-in-scope}
- **Outside a workflow body** — an ordinary function, an action, a page. There the caller *is* the current principal:
  write `Session.CurrentUser`.
- **In a `Candidates` predicate.** That predicate decides *about* a principal rather than running *because* of one:
  it is evaluated for every candidate when an inbox is listed, and once per principal at the deposit gate. The
  principal under test is the predicate's own parameter, so compare against that.

A local or parameter named `actor` shadows the ambient one, exactly as it would shadow any other name.

## Examples       {#examples}

### Naming the actor on a slot somebody else holds   {#outsider}
The discriminating case. The slot is assigned to one person; somebody else acts on it. Both facts are recorded, and
they are different:

```osy title="who it was assigned to, and who actually did it" test app=workflow-actor-outsider
enum Stage { Held, Done }

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

entity Claim {
  [Required, MaxLength(120)] string Title;
  Stage State;
  [Required] Person Owner;
  Person DecidedBy;    // who acted
  Person Queue;        // whose queue it sat in
  security { allow read, create, update when IsAuthenticated; }
}

workflow ClaimFlow {
  Tracks    = Claim.State;
  Autostart = true;
  Initial   = Held;

  event Decide();

  state Held {
    subscribe Decide() as Reviewer {
      Assignee = this.Item.Owner;
    }

    on Reviewer {
      this.Item.DecidedBy = actor;             // the person who stepped in
      this.Item.Queue     = Reviewer.Assignee; // the person it was waiting on
      goto Done;
    }
  }

  terminal success Done { }
}
```

## See also       {#see-also}
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — who may hold or satisfy a slot
- [Assign — handing a slot to a named colleague](https://osysharp.com/reference/workflow/assign/) — moving a slot to somebody, and what `Assignee` means
- [For(entity).Audit](https://osysharp.com/reference/workflow/audit/) — the run's timeline, which records the same actor
- [What a workflow body may write](https://osysharp.com/reference/workflow/body-security/) — why a body runs on the engine's authority, and what that means for reads
- [Acting on an inbox row (deposit, claim, release)](https://osysharp.com/reference/workflow/inbox-act/) — claiming, releasing and acting from a queue


---

<!-- https://osysharp.com/reference/workflow/complete-when/ -->

# complete when (a state's own completion condition)

> Declare, once on a state, the condition under which that state is finished and where the run goes next. It is re-checked after anything happens in the state, so the handlers can just record what happened instead of each one deciding whether the work is over. Reach for it when a state ends because of a condition over your own data rather than because one particular event arrived.

<!-- id: workflow-complete-when · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/complete-when/ -->

## Summary        {#summary}
Some states end because a particular thing happened. Others end because the world reached a particular shape — every
item settled, every reviewer answered, nothing left outstanding. For the second kind, the state is not waiting for an
*event*; it is waiting for a *condition*.

**`complete when (<predicate>) goto <State>;`** says exactly that, once, on the state. After anything happens in that
state the predicate is re-checked, and when it holds the run moves on.

## Signature      {#signature}
```osy syntax
state <Name> {
  complete when (<predicate over this.Item>) goto <State>;
  …
}
```
One per state. The predicate is an ordinary boolean expression with `this.Item` — the row the workflow tracks — in
scope, so it can call your own functions and read your own tables.

## Description    {#description}
Without it, the condition has to be repeated at the end of **every** handler that could be the last thing to happen:

```osy title="the same question, asked in three places" syntax app=drop-ship-order
on ShipmentDelivered(Shipment shipment) { …; when (AllItemsSettled(this.Item)) { goto Settled; } }
on ShipmentLost(Shipment shipment)      { …; when (AllItemsSettled(this.Item)) { goto Settled; } }
on CustomerWithdrawsItem(OrderItem item){ …; when (AllItemsSettled(this.Item)) { goto Settled; } }
```

That works, and it quietly gets worse as the workflow grows. The obligation lands on every handler you add, and it is
invisible at the moment it matters: **add a fourth handler, forget the line, and the run stays in that state for
ever.** Nothing reports it — there is no error, no timeout, no failed step. It simply never finishes.

Declared once, the handlers go back to doing one job each:

```osy title="declared once; the handlers just record what happened" syntax
state Fulfilling {
  // Asked ONCE, on the state — not at the end of each handler. Re-checked after anything happens here.
  complete when (AllItemsSettled(this.Item)) goto Settled;

  on ShipmentDelivered(Shipment shipment)  { shipment.Delivered = true; }
  on ShipmentLost(Shipment shipment)       { shipment.Lost = true; }
  on CustomerWithdrawsItem(OrderItem item) { item.Withdrawn = true; }
}
```

⚠ **Written out rather than pulled from an app, because no shipped sample uses `complete when` yet** — the fence is
`preview` for that reason as well as the surface's own. When a sample adopts it, this becomes a `sample=` pull like
the rest, and stops being prose that can drift.

**An explicit `goto` still wins.** A state can be left for reasons that have nothing to do with being finished — a
cancellation, a customer giving up. A handler that decides to leave goes where it says; the completion condition is
only consulted when the handler did not already transition.

**When it is checked.** After a handler for that state has run and its changes are committed — so the predicate sees
the world that handler left, including the rows it just wrote. It is not a poll: nothing re-checks it while the state
is idle, because nothing has changed.

**A condition that already holds ends the state at the first opportunity.** A job whose work is already complete is
complete; that is not a special case to guard against.

## Examples       {#examples}
```osy title="the state completes when the rule holds" test app=workflow-complete-when
enum Decision   { Approve, Reject }
enum OrderState { Fulfilling, Shipped }

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

entity Order {
  [Required, MaxLength(60)] string Reference;
  [Required] Person Requester;
  OrderState Status;                       // no default: the workflow owns this field
  security { allow read, create, update when IsAuthenticated; }
}

workflow Fulfilment {
  Tracks    = Order.Status;
  Autostart = true;
  Initial   = Fulfilling;

  event Pack();

  state Fulfilling {
    subscribe Pack();
    on Pack { }

    Requires {
      Packed { Must    = this.Item.Reference != "";
               Message = "Every line has to be packed first."; }
    }
    on Complete { goto Shipped; }
  }
  terminal success Shipped { }
}
```

An order that is finished when every one of its items has come to rest — whichever way each one got there, and
however many shipments it took:

```osy title="waiting for a condition, not for a set of work" syntax app=drop-ship-order
state Fulfilling {
  enter { SourcePendingItems(this.Item); }

  complete when (AllItemsSettled(this.Item)) goto Settled;

  subscribe ShipmentDelivered(Shipment shipment);
  subscribe CustomerWithdrawsItem(OrderItem item);

  on ShipmentDelivered(Shipment shipment) { … }   // records deliveries
  on CustomerWithdrawsItem(OrderItem item) { … }  // records a refund
}
```

## Notes          {#notes}
**This is not a join, and the difference is worth knowing.** A join is for *"I started this specific set of work and I
am waiting for it to come back."* A completion condition is for *"I am waiting for my own data to reach a shape."* The
order above cannot use a join: the number of shipments is unknowable when it starts, shipments come and go underneath
it, and a lost one puts its items back into the pool to be sent again. What the order waits for is a fact about its
items, not a set of tasks.

**It is not valid on a `terminal`.** A terminal is where a run ends, so there is no completion left to condition;
declaring one there is a compile error rather than a line that is silently ignored.

## See also       {#see-also}
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — declaring what a state waits for
- [fan-out (foreach subscribe)](https://osysharp.com/reference/workflow/fan-out/) — one slot per element, when you *are* waiting on a set of others
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — starting child work from a state


---

<!-- https://osysharp.com/reference/workflow/fan-out-dynamic/ -->

# dynamic fan-out (foreach over a runtime collection)

> A `subscribe … foreach` over a RUNTIME entity collection expands into one wait slot per element of the collection, resolved at state entry. Each instance can be pre-assigned to its own element (`Assignee = x`), and the whole fan-out is addressed by its base alias — the engine picks the instance for the acting principal.

<!-- id: workflow-fan-out-dynamic · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/fan-out-dynamic/ -->

## Summary        {#summary}
Where the [fan-out (foreach subscribe)](https://osysharp.com/reference/workflow/fan-out/) over a **literal enum list** is fixed at compile time and addressed by member name,
a fan-out over a **runtime entity collection** (`foreach (User u in this.Item.Topic.Referees)`) is resolved at
**state entry**: the engine evaluates the collection and materializes **one runtime slot per element**. The element
is an entity, so each instance can be **pre-assigned to its own element** (`Assignee = u`) and its `Candidates`
predicate can reference that element (`c => c == u || c == this.Item.Topic.BackupReferee`). Because the participants
are not known until run time, the fan-out has no statically-named instances — it is addressed by its **base alias**,
and the engine selects the instance for the **acting principal**.

## Signature      {#signature}
```osy syntax
subscribe <Event>(<typed params>) as <Alias>
  foreach (<EntityType> <var> in <this.Item…runtime collection>) {
    Assignee   = <var>;                          // pre-assign each instance to its element
    Candidates = <principal> => <predicate using var>;   // the eligible pool for that instance
  }
```

## Description    {#description}
The collection expression may be any runtime entity collection reachable from `this.Item` (a navigation, a
sub-collection). It must be a collection whose **element type matches the `foreach` variable type** — a
`foreach (User u in …)` must iterate a collection of `User`, or the workflow does not compile. The literal-enum form
and this dynamic form share the same `foreach` grammar; the compiler distinguishes them by the collection
expression (a literal enum-member list is static, anything else is dynamic).

- **One slot per element, resolved once at state entry.** The engine evaluates the collection when the state is
  entered and creates a slot for each element. The set of participants is a **snapshot** — adding a row to the
  collection afterwards does not grow a new slot for the current run.
- **Per-element key = the element's Id.** Each instance is tagged with its element entity's Id (not a member name).
- **Pre-assignment.** With `Assignee = <var>`, each instance opens **Assigned** to its element; without it the
  instance opens **Unassigned** and is claimed/deposited by whoever its `Candidates` admit.
- **Actor-based addressing.** A driver names the fan-out by its **base alias**
  (`PeerReview.For(paper).Referees.Review(Decision.Approve)`), and the engine routes the deposit to the instance
  **assigned to the acting principal** — or, failing that, the instance whose `Candidates` admit the actor (the
  fallback pool). A principal in no instance's pool is **refused** (a security refusal — see [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/)).
- **The element variable.** Inside `Candidates` and `Assignee` the loop variable is the element for THAT instance,
  so each slot gates and pre-assigns to its own referee.

Pair a dynamic fan-out with a state-level [Requires — named preconditions, and the live checklist](https://osysharp.com/reference/workflow/requires/) to express a **quorum** over a set whose size is
unknown at compile time ("3 approving reviews"), counting DATA rows recorded by the deposit arm. A per-element
breach arm ([Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) `Finished { Unfinished(Slot slot) { … } }`) receives the specific instance that
broke, so a late referee's slot can be reassigned to a backup without disturbing the others.

## Examples       {#examples}
One referee slot per element of `this.Item.Topic.Referees`, each pre-assigned, with a 3-approval quorum and a
breach that reassigns an unfinished slot to the backup:

```osy title="one slot per referee, with a 3-approval quorum" test app=workflow-fan-out-dynamic
enum Decision    { Approve, Reject }
enum PaperStatus { Refereeing, Decided, Withdrawn }

// Everything the workflow reaches through — declared here because the example uses all of it.
[Principal]
entity User {
  [Required, MaxLength(200)] string Email;
  Topic Topic;                 // the back-reference `[ForeignKey(Topic)] User[] Referees` points at
  security { allow read, create when IsAuthenticated; }
}

entity Topic {
  [Required, MaxLength(120)] string Name;
  [ForeignKey(Topic)] User[] Referees;
  User BackupReferee;
  security { allow read, create when IsAuthenticated; }
}

entity Paper {
  [Required, MaxLength(200)] string Title;
  [Required] Topic Topic;
  PaperStatus Status;   // no default: the workflow owns this field
  [ForeignKey(Paper)] Review[] Reviews;
  security { allow read, create, update when IsAuthenticated; }
}

entity Review {
  [Required] Paper Paper;
  User Referee;
  Decision Decision;
  security { allow read, create when IsAuthenticated; }
}

workflow PeerReview {
  Tracks    = Paper.Status;
  Autostart = true;
  Initial   = Refereeing;

  event Review(Decision decision);

  state Refereeing {
    Expire = TimeSpan.FromDays(60);

    // one slot per referee, resolved at state entry, each pre-assigned to its element
    subscribe Review(Decision decision) as Referees foreach (User u in this.Item.Topic.Referees) {
      Assignee   = u;
      Candidates = c => c == u || c == this.Item.Topic.BackupReferee;   // the fallback pool

      Finished {
        Within = TimeSpan.FromDays(14);
        Unfinished(Slot slot) { slot.Assign(this.Item.Topic.BackupReferee); }   // no goto → keep waiting
      }
    }

    // recording a review always works — it is just DATA
    on Review(Decision decision, Slot slot) {
      new Review { Paper = this.Item, Referee = slot.Assignee, Decision = decision };
    }

    // the state completes when 3 approvals hold — the rest never have to answer
    Requires {
      Quorum { Must    = this.Item.Reviews.Count(r => r.Decision == Decision.Approve) >= 3;
               Message = "Three approving reviews are required."; }
    }

    on Complete { goto Decided; }
    on Expire   { goto Withdrawn; }
  }

  terminal success Decided   { }
  terminal cancel  Withdrawn { Message = "not enough referees responded"; }
}
```

Driving it — a referee deposits on their own instance by the **base alias**; the engine picks the instance by the
acting principal:

```osy title="each referee deposits on the base alias" syntax
runas (Ravi) { PeerReview.For(paper).Referees.Review(Decision.Approve); }
runas (Bina) { PeerReview.For(paper).Referees.Review(Decision.Approve); }
runas (Cora) { PeerReview.For(paper).Referees.Review(Decision.Approve); }
// three approvals ⇒ Decided. A principal in no referee pool is refused.
```

## See also       {#see-also}
- [fan-out (foreach subscribe)](https://osysharp.com/reference/workflow/fan-out/) — the static literal-enum-list form (addressed by member name)
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — the per-instance eligible pool (and the fail-closed refusal)
- [Requires — named preconditions, and the live checklist](https://osysharp.com/reference/workflow/requires/) — the state-level quorum that counts the recorded reviews
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — the per-element `Finished { Unfinished(Slot slot) { … } }` breach arm


---

<!-- https://osysharp.com/reference/workflow/enter-exit/ -->

# enter and exit (a state's arrival and departure hooks)

> Run code when a run arrives in a state, and when it leaves. enter may redirect the run somewhere else; exit may not, because by the time it runs the move has already been decided. Both are optional and neither changes when the state's deadline clock counts.

<!-- id: workflow-enter-exit · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/enter-exit/ -->

## Summary        {#summary}
A state can run code at two moments: when a run **arrives** in it, and when a run **leaves** it.

```osy syntax
state UnderReview {
  enter { this.Item.ReviewStartedAt = DateTime.UtcNow; }
  exit  { this.Item.ReviewMinutes = (DateTime.UtcNow - this.Item.ReviewStartedAt).TotalMinutes; }
}
```

Both are optional, and both run inside the same transaction as the move that triggered them — so if either throws,
the move does not happen at all.

## Signature      {#signature}
```osy syntax
state <Name> {
  enter { <statements> }    // on arrival — MAY `goto` to redirect the run
  exit  { <statements> }    // on departure — may NOT `goto`
}
```

## Description    {#description}

### Can `enter`/`exit` redirect with `goto`?     {#goto}
`enter` may redirect: `enter { if (order.Total > 10000) goto NeedsApproval; }` sends the run somewhere else instead of
settling in this state. That works because on arrival, **where the run ends up is still the thing being decided**.

`exit` may not, and a `goto` in one is a compile error. By the time an exit body runs the transition has **already**
been decided — something chose the destination, and this body is running *because* of that choice. A redirect here
would either silently override a decision already made, or, if two states' exit bodies each redirected into the
other, bounce between them for ever with no arm to break the cycle.

The refusal names where the decision does belong:

```text
an `exit { }` body may not `goto` — state 'Open' runs its exit body BECAUSE a transition was already decided, so a
`goto` here would override a choice already made (and two states redirecting to each other would never settle). Put
the decision where it is still open: an `on` arm, a `complete when`, or the destination state's own `enter { }`,
which MAY redirect.
```

It reads the same way as the rule for milestones, which likewise may not `goto` from their `enter`: **a body that runs
as a consequence of a decision does not get to re-make it.**

### In what order do `exit`, `enter` and the deadline run?     {#order}
Leaving `A` for `B` runs, in one transaction:

1. `A`'s `exit { }`
2. `B`'s `enter { }`
3. `B`'s waits and deadline are armed

So an exit body sees the world as it was in the state it is leaving, before anything about the destination applies.

### Deadlines and `Accrues`     {#clocks}
A state's SLA clock has already **stopped** by the time its exit body runs. `Accrues` measures time spent *in* the
state, and work done on the way out is not that — so an exit body can never inflate the very measurement it is often
there to record.

### What happens if a body reaches the network?     {#network}
Wrap it. A body here is not an ordinary function: **nobody called it**, so there is nobody to hand a failure to.

A timeout, a DNS failure or a refused connection is not a workflow outcome — the platform treats it as worth another
attempt, so the delivery is retried and **this whole body runs again from the top**, with the row still sitting in the
state it was arriving in. That is right for a blip and wrong for an endpoint that is simply gone, and either way it is
not what you would have chosen if you had been asked.

`osy lint` asks for you, in every body a workflow declares — `enter`, `exit`, an `on` route, a milestone arm, a
`Remind`. Three findings cover the two ways a call can go wrong, and they cover the reach **through a helper** as well
as the call written here, because a body that says only `SendMail(this.Item)` warns you of nothing in its own text:

| finding | what it saw |
|---|---|
| `reliability-outbound-call-unguarded` | an outbound call written in this body, with no `try` |
| `reliability-reaches-the-network-unguarded` | this body reaches the network **through something it calls**, with no `try` |
| `reliability-http-result-unchecked` | an `Http.*` result used without asking whether the call worked — a 404 is a normal return and no `try` can catch it |

A helper that catches everything itself counts as handled, however many hops down it is, so you write the guard once
where it means something rather than at every level.

```osy title="a state that mails on arrival — the guard is the difference between a nudge and a stuck run" test app=workflow-enter-exit
class MailRequest { public string To; public string Subject; }
class MailResult  { public string Id; }

client Mailer {
  BaseUrl = "https://api.mail.example";
  [Post("/send")] MailResult Send(MailRequest body);
}

entity Invitation {
  [Required, MaxLength(200)] string Email;
  InviteStatus Status;   // no default: `InviteFlow` autostarts, so its `Initial` IS this field's first value
}
enum InviteStatus { Pending, Accepted }

// The guard lives HERE, in the one place that knows what a failed send means: the invitation is still open, the run
// still moves on, and the failure is findable. Without it the send is retried with everything above it.
void SendInvite(Invitation inv) {
  try {
    var sent = Mailer.Send(new MailRequest { To = inv.Email, Subject = "You are invited" });
  }
  catch (Exception e) {
    Log.Error(e, "invitation mail failed for {Email}", inv.Email);
  }
}

workflow InviteFlow {
  Tracks    = Invitation.Status;
  Autostart = true;
  Initial   = Pending;

  event Accept();

  state Pending {
    enter { SendInvite(this.Item); }
    subscribe Accept() as Acceptance;
    on Acceptance { goto Accepted; }
  }

  terminal success Accepted { }
}
```

### Do `enter`/`exit` run on a terminal state?     {#terminals}
A terminal state can declare an `enter { }`, which runs as the run finishes. It can declare an `exit { }` too, but
nothing will ever run it: a terminal is where runs stop. Prefer putting the work in `enter`.

## Examples       {#examples}

Recording how long a review took — the pair working together, which is what `exit` exists for:

```osy title="timing-a-state" test app=workflow-enter-exit
entity Review {
  [Required, MaxLength(200)] string Title;
  DateTime? StartedAt;
  int Minutes;
  ReviewStatus Status = ReviewStatus.Pending;
}
enum ReviewStatus { Pending, UnderReview, Done }

workflow ReviewFlow {
  Tracks    = Review.Status;
  Autostart = false;
  Initial   = Pending;

  event Begin();
  event Finish();

  state Pending {
    subscribe Begin() as B;
    on B { goto UnderReview; }
  }

  state UnderReview {
    enter { this.Item.StartedAt = DateTime.UtcNow; }
    // Runs on the way out, whichever arm caused the move — so a second way out of this state cannot forget it.
    exit  { this.Item.Minutes = this.Item.Minutes + 1; }

    subscribe Finish() as F;
    on F { goto Done; }
  }

  terminal success Done { }
}
```

That last point is the practical argument for `exit` over repeating the line at the end of every arm: a state with
three ways out needs the bookkeeping written once, and a fourth arm added later cannot silently omit it.

## See also       {#see-also}
- [complete when (a state's own completion condition)](https://osysharp.com/reference/workflow/complete-when/) — ending a state on a condition rather than on a particular event
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — `Assigned`/`Finished` timers, whose own `enter` may not `goto` either
- [ServiceHours (SLA-accrual windows)](https://osysharp.com/reference/workflow/service-hours/) — what `Accrues` measures, and therefore what an exit body is outside of
- [Tracks and Initial (the field a workflow drives)](https://osysharp.com/reference/workflow/tracks/) — the enum whose members these states are
- [a typed HTTP client (client)](https://osysharp.com/reference/http/client/) — declaring the typed client a body like the one above calls
- [When a child is cancelled or fails](https://osysharp.com/reference/workflow/fault-propagation/) — what an uncaught *workflow* outcome does, which is not what a network fault does


---

<!-- https://osysharp.com/reference/workflow/fan-out/ -->

# fan-out (foreach subscribe)

> One `subscribe` declaration that expands into MANY parallel wait slots — one per element of a collection. A fan-out over a literal enum list makes each instance addressable by the element's NAME (`Wf.For(entity).Architect.Event(…)`), and the element variable is in scope for the slot's `Candidates`.

<!-- id: workflow-fan-out · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/fan-out/ -->

## Summary        {#summary}
A `foreach` on a `subscribe` fans one declaration out into MANY parallel wait slots — one runtime slot per element
of the collection. It is how a state waits on several symmetric approvers at once (three reviewers, N referees)
without copy-pasting the slot three times. Over a **literal enum list** each instance is addressable by the element's
member name, and the loop variable is in scope for the slot's `Candidates`.

## Signature      {#signature}
```osy syntax
subscribe <Event>(<typed params>) as <Alias>
  foreach (<ElementType> <var> in [<A>, <B>, <C>]) {
    Candidates = <principal> => <predicate using var>;
  }
```

## Description    {#description}
The `foreach` element type and variable are declared C#-style. At state entry the engine evaluates the collection
and materializes **one runtime slot per element**, each tagged with that element as its key. The loop variable is a
resolvable value **in scope** for the slot's settings — `Candidates`, `Assignee`, `When` — so a single predicate
specializes per instance:

- **Addressing (literal enum list).** The instance alias is the **enum member name**. `Wf.For(entity).Architect`
  targets the slot whose element is `Role.Architect`; its deposit verb is the subscribed event
  (`Wf.For(entity).Architect.Vote(Decision.Approve, "lgtm")`). The set of instance aliases is exactly the enum
  members in the list.
- **The element variable.** Inside `Candidates = u => u.Role == r`, `r` is the element for THAT instance, so the
  Architect slot only accepts a principal whose `Role == Role.Architect`.

Each instance is an ordinary slot: it can be claimed, deposited into, and gated by `Requires`. The route that fires
on a deposit is keyed on the **event** (`on <Event> { … }`) so all instances share one arm; a trailing `Slot slot`
parameter on that route names WHICH instance answered (`slot.Assignee` is the depositor).

Casting into an instance does not, by itself, advance the state. Pair a fan-out with a state-level
[Requires — named preconditions, and the live checklist](https://osysharp.com/reference/workflow/requires/) to express a **quorum** (“2 of 3 approvals”) — that requirement holding is the state's
completion condition, so the remaining instances never have to answer.

A fan-out over a **runtime collection** (`foreach (User u in this.Item.Topic.Referees)`) is a separate, dynamic form:
one slot per element resolved at state entry, each pre-assignable to its element, addressed by the acting principal
rather than by a static name. See [dynamic fan-out (foreach over a runtime collection)](https://osysharp.com/reference/workflow/fan-out-dynamic/).

## Examples       {#examples}
Three symmetric voter slots from one declaration, each gated to its own role, with a 2-of-3 quorum as the
completion condition:

```osy title="three role-gated slots from one declaration" test app=workflow-fan-out
enum Decision { Approve, Reject }
enum Role     { Architect, Security, Product }
enum ReviewState { Gathering, Accepted, Rejected }

// The entity the workflow TRACKS, and the person it assigns slots to. The example referenced both
// and declared neither — which only went unnoticed while it was exempt from the gate.
[Principal]
entity Person {
  [Required, MaxLength(200)] string Email;
  Role Role = Role.Architect;
  security { allow read, create when IsAuthenticated; }
}

entity Design {
  [Required, MaxLength(160)] string Title;
  ReviewState Status;   // no default: the workflow owns this field (Tracks = Design.Status)
  [ForeignKey(Design)] Vote[] Votes;
  security { allow read, create, update when IsAuthenticated; }
}

entity Vote {
  [Required] Design Design;
  Person Voter;
  Decision Decision;
  [MaxLength(400)] string Comment;
  security { allow read, create when IsAuthenticated; }
}

workflow DesignReview {
  Tracks    = Design.Status;
  Autostart = true;
  Initial   = Gathering;

  event Vote(Decision decision, string comment);

  state Gathering {
    // one declaration → three parallel slots, addressable as .Architect / .Security / .Product
    subscribe Vote(Decision decision, string comment) as Voters
      foreach (Role r in [Role.Architect, Role.Security, Role.Product]) {
        Candidates = u => u.Role == r;
      }

    on Vote(Decision decision, string comment, Slot slot) {
      new Vote { Design = this.Item, Voter = slot.Assignee, Decision = decision, Comment = comment };
    }

    // the STATE completes when the quorum of approvals holds — the third voter never has to answer
    Requires {
      Quorum { Must = this.Item.Votes.Count(v => v.Decision == Decision.Approve) >= 2;
               Message = "Two of three approvals are required."; }
    }

    on Complete { goto Accepted; }
  }

  terminal success Accepted { }
  terminal error   Rejected { Message = "design rejected"; }
}
```

Driving it from a test (or a UI) — a voter casts on THEIR instance by its role name:

```osy title="a voter casts on their own instance by role name" syntax
runas (Ada) { DesignReview.For(design).Architect.Vote(Decision.Approve, "lgtm"); }
runas (Sam) { DesignReview.For(design).Security.Vote(Decision.Approve, "ok"); }
// two of three approvals ⇒ Accepted; Product never voted
```

## See also       {#see-also}
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the wait this fans out
- [Requires — named preconditions, and the live checklist](https://osysharp.com/reference/workflow/requires/) — the state-level quorum that is the completion condition
- <span class="planned" title="this page is planned and not written yet">workflow-route</span> — the event-keyed `on <Event> { … }` arm shared by every instance (and its `Slot slot` param)
- <span class="planned" title="this page is planned and not written yet">workflow-state</span> — the enclosing state


---

<!-- https://osysharp.com/reference/workflow/slot-dependencies/ -->

# slot dependencies (After / When / Pending)

> Per-slot ordering and conditioning. `After = [A, B]` holds a slot CLOSED (status `Pending`, no clock) until every named sibling slot is satisfied; `When = <predicate>` decides — WHEN THE SLOT WOULD OPEN — whether it exists at all. Together they express the parallel-then-serial shape a real approval chain has.

<!-- id: workflow-slot-dependencies · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/slot-dependencies/ -->

## Summary        {#summary}
Two per-slot settings order and condition a wait. `After = [A, B]` makes a slot open only once every named sibling
slot is satisfied — until then it sits **`Pending`**: visible, but not claimable and with no SLA clock running.
`When = <predicate>` decides whether the slot EXISTS at all, evaluated **when the slot would open** (not at state
entry) — so a value that changes mid-review (an expense amount raised past a threshold) correctly grows a slot that
did not exist before. Slots with neither open immediately, in parallel.

## Signature      {#signature}
```osy syntax
subscribe <Event>(<typed params>) as <Alias> {
  When  = <predicate over this.Item>;      // the slot only EXISTS while this holds
  After = [<SiblingAlias>, <SiblingAlias>]; // …and only OPENS once all of these are satisfied
}
```

## Description    {#description}
- **`After` — a dependency, not a mode.** `After` names sibling slots of the same state. A slot with predecessors is
  created **`Pending`** at state entry and cannot be claimed or deposited into (a deposit is refused, like an
  out-of-pool one); no clock runs, so it can never breach for a predecessor's slowness. When the LAST predecessor is
  satisfied the slot **opens** — `Unassigned` (or `Assigned` if it declares an `Assignee`) — and its SLA clock starts.
- **`When` is evaluated at OPEN time.** For a slot with `When` and `After`, the condition is checked at the moment its
  predecessors complete, over the current `this.Item`. A slot whose `When` is false is **never created** — not
  `Pending`, not shown, not counted toward completion. If the condition only becomes true later (the amount was
  raised), the slot **grows** then. For a slot with `When` but no `After`, the condition is evaluated at state entry.
- **Completion.** `on Complete` fires when every slot that EXISTS AND IS OPEN is satisfied. A `When`-false (never
  created) slot does not block completion; a `Pending` or open-but-unsatisfied slot does.
- **Observing it — `SlotStatus` + `.Slots`.** The `SlotStatus` enum (`Pending`, `Unassigned`, `Assigned`,
  `Satisfied`, `Cancelled`, `Breached`) is the type of `Wf.For(entity).<Slot>.Status`. `Wf.For(entity).Slots` returns
  the run's live slots (`List<SlotView>`, each with `.Name` and `.Status`) — a slot the `When` gate never created is
  simply absent, so `Wf.For(e).Slots.Any(s => s.Name == "Cfo")` is false for an expense below the threshold.

## Examples       {#examples}
Manager and Finance approve in parallel; the CFO slot opens only after both, and only for large expenses:

```osy title="a slot that opens only after both, and only if large" test app=workflow-slot-dependencies
enum Decision      { Approve, Reject }
enum RequisitionStatus { Approvals, Approved, Rejected }
enum Role          { Staff, Finance, Cfo }
enum Dept          { Engineering, Finance, Legal }

// The entities the workflow reaches into. The example named all four and declared none — which the
// gate could not see while the fence was exempt from it.
[Principal]
entity Person {
  [Required, MaxLength(200)] string Email;
  Role Role = Role.Staff;
  Dept Department = Dept.Engineering;
  Person Manager;
  security { allow read, create when IsAuthenticated; }
}

entity Requisition {
  [Required] Person Employee;
  decimal Cost;
  RequisitionStatus Status;   // no default: the workflow owns this field
  security { allow read, create, update when IsAuthenticated; }
}

entity Approval {
  [Required] Requisition Requisition;
  Person By;
  DateTime At;
  security { allow read, create when IsAuthenticated; }
}

workflow RequisitionApproval {
  Tracks    = Requisition.Status;
  Autostart = true;
  Initial   = Approvals;

  event Approve(Decision decision);

  state Approvals {
    subscribe Approve(Decision decision) as Manager {
      Assignee = this.Item.Employee.Manager;
    }
    subscribe Approve(Decision decision) as Finance {
      Candidates = u => u.Department == Dept.Finance;
    }
    // Pending until BOTH predecessors are satisfied — and only exists for large expenses.
    subscribe Approve(Decision decision) as Cfo {
      When       = this.Item.Cost > 10000;
      After      = [Manager, Finance];
      Candidates = u => u.Role == Role.Cfo;
    }

    on Approve(Decision decision, Slot slot) {
      new Approval { Requisition = this.Item, By = slot.Assignee, At = DurableClock.Now };
    }
    on Complete { goto Approved; }
  }

  terminal success Approved { }
  terminal error   Rejected { Message = "expense rejected"; }
}
```

Observing the CFO slot's lifecycle from a test (or a UI):

```osy title="watching Pending open the moment predecessors land" syntax
Assert.Equal(SlotStatus.Pending, RequisitionApproval.For(e).Cfo.Status);      // visible, not open
runas (Mia)  { RequisitionApproval.For(e).Manager.Approve(Decision.Approve); }
Assert.Equal(SlotStatus.Pending, RequisitionApproval.For(e).Cfo.Status);      // one predecessor down
runas (Otto) { RequisitionApproval.For(e).Finance.Approve(Decision.Approve); }
Assert.Equal(SlotStatus.Unassigned, RequisitionApproval.For(e).Cfo.Status);   // NOW it opens — clock starts

// a small expense never grows a CFO slot at all
Assert.False(RequisitionApproval.For(small).Slots.Any(s => s.Name == "Cfo"));
```

## See also       {#see-also}
- [subscribe](https://osysharp.com/reference/workflow/subscribe/) — the wait these settings condition
- [Requires — named preconditions, and the live checklist](https://osysharp.com/reference/workflow/requires/) — the other completion gate (a quorum predicate)
- <span class="planned" title="this page is planned and not written yet">workflow-route</span> — the event-keyed arm shared by the sibling slots (and its `Slot slot` param)
- [fan-out (foreach subscribe)](https://osysharp.com/reference/workflow/fan-out/) — many parallel slots from one declaration
- <span class="planned" title="this page is planned and not written yet">workflow-state</span> — the enclosing state


---

<!-- https://osysharp.com/reference/workflow/subscribe/ -->

# subscribe

> Declares that a workflow state waits on an event, and configures the wait — who may hold it, who may hand it on, whether it is armed at all, and what must be satisfied first. A subscribe holds CONFIG only: the routes that fire when the event arrives live at the state level, and the SLA promises live in its own Assigned/Finished blocks.

<!-- id: workflow-subscribe · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/subscribe/ -->

## Summary        {#summary}
`subscribe` declares that a workflow state is listening for an event, and configures that wait — who may fill it, when
it is armed, what must hold first, and what it promises. It is **config only**: the guarded routes that fire when the
event arrives (`on Event when (…) { … goto … }`) live at the **state** level alongside the wait, not inside the
`subscribe` block.

## Signature      {#signature}
```osy syntax
subscribe <Event>(<typed params>) [as <Alias>] [foreach (<T> <x> in <collection>)] {
  Candidates = <principal => bool> | <() => List<Principal>>;   // who may HOLD or satisfy it
  Reassign   = <actor => bool>;                                 // who may hand it ON
  Assignee   = <expr>;                                          // who it is handed to at arm time
  When       = <predicate>;                                     // whether it is armed on this run at all
  After      = [<Slot>, …];                                     // slots that must be satisfied first

  Assigned { … }   // the pickup promise — Within, Remind, a breach arm
  Finished { … }   // the completion promise
  Requires { … }   // named preconditions on filling it
}

subscribe <Event>();   // bodyless — a wait with no configuration
```

## Description    {#description}
A workflow state advances when an event it subscribes to arrives and a matching route transitions it. `subscribe`
names that event and restates its typed parameters (so the payload is visible at the handler), then configures the
wait itself. Each setting has its own page — see [See also](#see-also) — and the shape to hold is that they answer
different questions:

| setting | question |
|---|---|
| [`Candidates`](https://osysharp.com/reference/workflow/candidates/) | who may **hold or satisfy** this slot |
| [`Reassign`](https://osysharp.com/reference/workflow/assign/) | who besides the holder may **move** it |
| `Assignee` | who it belongs to from the moment it is armed (a slot with one is not a pool slot) |
| `When` | whether this run gets the slot **at all** |
| [`After`](https://osysharp.com/reference/workflow/slot-dependencies/) | which sibling slots must be satisfied before it opens |

The **promises** — how long a slot may sit unclaimed, how long its holder has, the nudges along the way, the retries,
and where a missed one routes — are declared in the [`Assigned`/`Finished`](https://osysharp.com/reference/workflow/milestone/) blocks inside the
slot, not as flat settings on it. That is where `Within`, `Remind`, `Retries`, `Backoff` and the `Unassigned` /
`Unfinished` / `Exhausted` arms live.

A slot's **success** routes (`on <Event> when (…)`, `on <Alias>(…)`) and the state's own timers (`on Expire`,
`on Deadline`) live at the **state** level, not inside the `subscribe` — every `on …` in one place.

An **entity-typed parameter is a live row, not a copy**. A route may read it and write through it, exactly as it can
through the workflow's own tracked item, and those writes are saved with the rest of the transition:

```osy title="an event that carries the row it is about" syntax app=order-fulfillment
event CustomerWithdrawsItem(OrderItem item);

on CustomerWithdrawsItem(OrderItem item) {
  item.Status = ItemStatus.Withdrawn;    // persisted with the transition
  RefundItem(item);
}
```

The event's delivery context (who deposited, the claim) is available to a route that opts into a `Deposit`
parameter — see <span class="planned" title="this page is planned and not written yet">workflow-route</span>; it is never ambient.

A state may hold more than one `subscribe` (multi-wait / N-of-M); that is its own surface — see <span class="planned" title="this page is planned and not written yet">workflow-multiwait</span>.

## Examples       {#examples}
A pool slot the support team may take, a supervisor may move, and which promises to be picked up within an hour:

```osy title="a pool slot with a gate, a hand-over rule and a promise" syntax
subscribe Respond() as Reply {
  Candidates = u => u.Team == Team.Support && u.OnDuty;
  Reassign   = a => a.IsSupervisor;

  Assigned { Within = TimeSpan.FromHours(1); Unassigned { goto Escalated; } }
}
```

The same wait as an app writes it, in context:

```osy title="a guarded approval wait" syntax sample=wf-approvals/model/po_approval.osy#draft-state
```

Minimal — a wait with no config, just the event:

```osy title="minimal" syntax app=order-fulfillment
subscribe Submit();
```

## See also       {#see-also}
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — the slot's `Candidates` gate: who may HOLD/satisfy this slot (predicate or computed
  set), and how a screen ASKS it before offering a button: `<Wf>.For(item).<Slot>.Candidates(u)`
- [Assign — handing a slot to a named colleague](https://osysharp.com/reference/workflow/assign/) — `Reassign`, and the verbs that hand a slot to a named colleague
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — the `Assigned`/`Finished` promises declared inside the slot: `Within`, `Remind`, `Retries`,
  `Backoff`, and the breach arms
- [slot dependencies (After / When / Pending)](https://osysharp.com/reference/workflow/slot-dependencies/) — `After`: the sibling slots that must be satisfied before this one opens
- [Requires — named preconditions, and the live checklist](https://osysharp.com/reference/workflow/requires/) — `Requires`: named preconditions on filling the slot, and the live checklist that reports them
- <span class="planned" title="this page is planned and not written yet">workflow-event</span> — the `event` this subscribes to (a method signature)
- <span class="planned" title="this page is planned and not written yet">workflow-route</span> — the state-level `on <Event> … goto …` routes that consume the deposit
- <span class="planned" title="this page is planned and not written yet">workflow-state</span> — the enclosing state and its `Expire` deadline
- [Tracks and Initial (the field a workflow drives)](https://osysharp.com/reference/workflow/tracks/) — how the workflow binds to an entity's enum property
