# 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
