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: a function that fails half-way leaves nothing behind. Whether its writes are stored when it returns depends on who called it, and that is the one thing to know:
- Called on its own — from a test, a workflow, an endpoint or schedule, a function running on the server, or from a page
with only plain values (
string,int, …) as arguments — it runs in a unit of work of its own, and the rows it writes are committed together when it returns. - Called from a page with a row as an argument (
void Strip(Route r)from anaction), it joins the page's unit of work: its writes show on screen at once and are stored only when that page commits. If nothing on the page callsUnitOfWork.Commit(), they are discarded when the page goes away — with no error, and with every test that only looks at the screen passing. End such a function (or the action) withUnitOfWork.Commit();.osy validateandosy testprint this case asDATA_NEVER_SAVED, naming the write.
The examples on this page are called on their own, so they need no commit. A body inside a component always works
in the page's unit of work — see creating & saving data and UnitOfWork. Pages are getting an explicit
unit-of-work mode; until then, the rule above is how it works.
Signature#
<ReturnType> <Name>(<Type> <param>, …) {
<statements>
}
Description#
How do I declare a function?#
void when it returns nothing, a type when it returns something. Parameters are typed and camelCase; the function
name is PascalCase:
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#
Everything a function writes lands together, or not at all. That is what lets you write the obvious thing:
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.
Does a function that calls out need async?#
No. A function that reaches outside the database — an HTTP call, an LLM completion — is written exactly like any
other function: no async, no Task<T>, and no colour to keep track of. (async is for running two calls side by
side — async / await and Task.)
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 and Task is the page to
read: it is the first habit to unlearn.
Where may a function be declared?#
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 Crossing the network | 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# for why.
int Doubled(int n) { return n * 2; } // a function
class Rates { public decimal WithVat(decimal net) { return net * 1.25m; } } // a class method
[Route("/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:
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.
See also#
- var — locals inside the body
- if / else · foreach · while — control flow
- class methods — behaviour attached to a type
- async / await and Task —
async,awaitandTask: running calls side by side - Running tests — how you run one