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; the essentials are below.
Signature#
entity <Name> {
<attributes>? <Type> <MemberName>;
}Description#
Which types may a member hold?#
This table is the whole storable set — Every type, in one list 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 |
double | measurements | a binary float — never money |
decimal | money and exact quantities | use this for money, never a floating type — decimal |
bool | true / false | |
DateTime | a point in time | stored UTC — DateTime |
DateOnly | a calendar date, no time | a SQL date — DateOnly and TimeOnly |
TimeOnly | a time of day, no date | a SQL time — DateOnly and TimeOnly |
TimeSpan | a duration | TimeSpan (durations) |
Guid | an identifier | |
Json | an arbitrary JSON document | for genuinely open-shaped data — Json |
Markdown | a section-addressable markdown document | text plus a rendering contract — Markdown |
RichText | formatted prose | a rich-text document |
Vector | an embedding, for similarity search | [MaxLength(n)] sets the dimensions — [Searchable] |
TimeZoneInfo | a time zone, stored as its IANA id (Europe/Stockholm) | Time zones — TimeZoneInfo |
Culture | a BCP-47 culture token (sv-SE) | Culture formatting — ToString(format, culture) |
byte[] | binary | the one array form that is a scalar, not a collection |
an enum you declared | a fixed set of values | see enum |
another entity | a reference to one row | see relations |
<Entity>[] | the children pointing back at this row | see relations |
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):
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 itselfA 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](/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](/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 and Optional and required members.
How do I give a member a default value?#
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:
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 ApiKeys; the
expression runs per row, not once. (A constant/enum default behaves the same as always.)
A default can also be one of the constant members C# gives the built-in types — Guid.Empty, string.Empty,
int.MinValue/int.MaxValue, long.MinValue/long.MaxValue, decimal.Zero/One/MinusOne/MinValue/MaxValue,
double.MinValue/double.MaxValue, DateTime.MinValue/DateTime.MaxValue, DateOnly.MinValue/DateOnly.MaxValue,
TimeOnly.MinValue/TimeOnly.MaxValue and TimeSpan.Zero/TimeSpan.MinValue/TimeSpan.MaxValue. Each stands for a
value that never changes, so it is stored exactly as the literal it names, on an entity and on a class alike:
public class Cursor {
public Guid After = Guid.Empty; // start from the beginning
public string Filter = string.Empty;
public int Limit = int.MaxValue;
public DateOnly Until = DateOnly.MaxValue; // open-ended
public TimeSpan Timeout = TimeSpan.MaxValue;
}
entity Reading {
[Required, MaxLength(80)] string Sensor;
Guid Batch = Guid.Empty; // no batch assigned yet
DateTime LastSeen = DateTime.MinValue; // never seen
decimal Offset = decimal.Zero;
}The constant must fit the member the way C# requires: long Big = int.MaxValue; widens and is fine, while
int Count = long.MaxValue; or string Name = Guid.Empty; is refused with both types named. DateTime.MaxValue is
the largest instant a stored date can hold, 9999-12-31 23:59:59.999999 — the same value it has in an expression.
For the same reason TimeOnly.MaxValue is 23:59:59.999999 and TimeSpan.MaxValue / MinValue are
±10675199.02:48:05.4775800. A boundary value is stored as the value it names, so a new row, a row an added member
backfilled, and a query that compares against the constant all agree.
Any other dotted default that is not an enum member or one of these constants is refused, and the message lists every
accepted form.
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#
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:
entity Article {
[MaxLength(200)] string Title; // bounded — a title has a sane maximum
string Body; // unbounded — prose
}Money is decimal#
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?#
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.[Mentions]declares the entities a rich-text member may@-mention, so an editor can offer and resolve them.
Every entity already has an automatic Id — read it like any other member. Nothing else is added: a creation time, a
creator and the last change are fields you declare with [CreatedAt]/[CreatedBy]/[ModifiedAt]/[ModifiedBy], and
the platform writes them ([CreatedAt] [CreatedBy] [ModifiedAt] [ModifiedBy]).
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
}How do I describe a member to whoever calls it?#
A /// doc comment on a member is its description, and [Example] gives it a representative value. Neither
changes what is stored — both travel with the member into the JSON schema a published function or an MCP tool
exposes, so the caller reading that schema sees what the field is for and what a real one looks like instead of a
bare type name.
entity Order {
/// The human-facing reference printed on the invoice.
[Required, MaxLength(50), Example("ORD-1042")] string Reference;
/// Where the goods are going.
[MaxLength(200), Example("Storgatan 1, 111 51 Stockholm")] string? ShipTo;
}See also#
- entity — the type these members live in
- constraints —
[Required],[Unique],[MaxLength],[Min]/[Max],[Pattern] - relations — members that point at another entity
- enum — a member with a fixed set of values