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

Reference / Entity

relations

[Required] <Parent> <Parent>; // the child points up [ForeignKey(<Parent>)] <Child>[] <Children>; // the parent reads down

One entity points at another by declaring it as a member — that is the foreign key. The parent reads its children back through a collection member marked with ForeignKey. Never query children with a filter; go through the collection.

stable3 examples compiled by CIentitymodelrelations

Summary#

A relation is two halves of one idea. The child points up by declaring the parent as a member — Order Order; — which is the foreign key. The parent reads down through a collection member — [ForeignKey(Order)] LineItem[] LineItems;. Declare both, and you can walk the graph in either direction.

Signature#

entity <Child> {
  [Required] <Parent> <Parent>;                    // the FK — one row of the parent
}

entity <Parent> {
  [ForeignKey(<Parent>)] <Child>[] <Children>;     // the children pointing back at this row
}

Description#

How do I declare both sides of a relation?#

The child's member IS the foreign key — there is no OrderId to declare and keep in step. You assign a row, not an id, and you read a row back. The parent's collection member carries the [ForeignKey] attribute, naming the relation it reads down — [ForeignKey(Order)] on the Order's Lines:

entity Order {
  [Required] string Code;
  decimal Total;
  [ForeignKey(Order)] LineItem[] Lines;   // read down: this order's lines
}

entity LineItem {
  [Required] Order Order;                  // point up: the order this line belongs to
  [MaxLength(200)] string Product;
  decimal Amount;
}

void AddLine(string orderCode, string product, decimal amount) {
  var order = Order.Single(o => o.Code == orderCode);
  var line = new LineItem { Order = order, Product = product, Amount = amount };
  //                        ^^^^^^^^^^^^^ assign the ROW, not an id
}

[Required] on the child's parent member means an orphan is impossible: a LineItem with no Order is a violation at commit, not a row nobody notices for six months.

Read children through the collection, never a filter#

This is the one rule people get wrong. To get an order's lines, go through the collection:

decimal OrderTotal(string code) {
  var order = Order.Single(o => o.Code == code);
  var total = 0m;
  foreach (var line in order.Lines) {     // the collection — connected to the order you already loaded
    total += line.Amount;
  }
  return total;
}

Do not write a standalone query filtered on the foreign key to fetch children. It looks equivalent and is not: the collection is part of the object graph you already have, so it is loaded once and reused, while a separate query is disconnected from the parent and re-runs every time you touch it. The collection is the connected path; a filtered query is a second, unrelated result set that happens to contain the same rows.

How do I let a reference be unset?#

Leave off [Required] and the reference may be null — a Ticket that nobody is assigned to yet:

entity Person {
  [Required] string Name;
}

entity Ticket {
  [Required] string Title;
  Person Assignee;                     // may be null — an unassigned ticket is a real state
}

string AssigneeName(Ticket t) {
  return t.Assignee?.Name ?? "unassigned";   // null-safe, exactly as in C#
}

What [Required] decides about deleting#

A required reference says the child cannot exist without its parent, and the platform takes that literally: deleting the parent deletes those children with it. An optional reference is the other answer — deleting the parent leaves the child and clears its reference.

entity Tag {
  [Required] Note Note;    // deleting the Note deletes this Tag
}

entity Draft {
  Note Note;               // deleting the Note leaves this Draft, with Note cleared
}

This is where the decision is made, so it is worth making deliberately. A cascade removes the children without asking whether the caller could have deleted them on their own — someone allowed to delete a Note can delete its Tags by deleting the note, even where your rules never grant them delete on Tag. That is the declaration doing what it says rather than a gap in it: the alternative would be a legal model that fails at run time, with nothing you could write to fix it.

So if a child's removal should be governed separately from its parent's, do not make its reference required — model it as optional and delete it explicitly.

See also#

Related

entity

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

entity members

The typed members an entity holds — text, numbers, dates, booleans, Guids, enums and references. A member's type…

Where / Single / Count

Query an entity by writing a predicate over it. The query runs in the database — not a filter over rows you already…