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] |
Zone | an IANA time-zone token (Europe/Stockholm) | Time zones — the Zone type and its operations |
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.)
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.[Id]marks a member as the entity's primary key. Every entity already has an automaticId, 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.
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#
- 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