Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Entity

entity members

<attributes> <Type> <Name>; // inside an entity

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]].

stable7 examples compiled by CIentitymodeltypes

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 setEvery 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.

TypeHoldsNotes
stringtextgive it a [MaxLength(n)] — see below
int · longwhole numberslong for ids and counters past 2³¹ — long
doublemeasurementsa binary float — never money
decimalmoney and exact quantitiesuse this for money, never a floating type — decimal
booltrue / false
DateTimea point in timestored UTC — DateTime
DateOnlya calendar date, no timea SQL dateDateOnly and TimeOnly
TimeOnlya time of day, no datea SQL timeDateOnly and TimeOnly
TimeSpana durationTimeSpan (durations)
Guidan identifier
Jsonan arbitrary JSON documentfor genuinely open-shaped data — Json
Markdowna section-addressable markdown documenttext plus a rendering contract — Markdown
RichTextformatted prosea rich-text document
Vectoran embedding, for similarity search[MaxLength(n)] sets the dimensions — [Searchable]
Zonean IANA time-zone token (Europe/Stockholm)Time zones — the Zone type and its operations
Culturea BCP-47 culture token (sv-SE)Culture formatting — ToString(format, culture)
byte[]binarythe one array form that is a scalar, not a collection
an enum you declareda fixed set of valuessee enum
another entitya reference to one rowsee relations
<Entity>[]the children pointing back at this rowsee 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 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](/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 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.
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

Related

entity

Declares a persisted type — a table of rows the app stores, queries and secures. Every entity gets an Id and audit…

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…

Every type, in one list

The complete vocabulary of built-in types — the scalars you can store, the collections, the two callable spellings…

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…

constraints

The per-member rules the database enforces — Required, Unique, MaxLength/MinLength, Min/Max, Pattern, and the…

relations

One entity points at another by declaring it as a member — that is the foreign key. The parent reads its children back…

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 visibility (public / internal)

A top-level type carries a public or internal visibility that decides whether code outside its namespace can name it…