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

Reference / Counter

Counter

Counter <Name> = new() { Start = <n>, Increment = <n>, Format = $"…{Value:D4}" }; [Counter(<Name>)] int|string <Member>; [Counter(<Name>, scope = <Ref>)] int <Member>; // restart per parent

The PLATFORM assigns this number at create and you never set it — invoice codes, ticket numbers. NOT a manual order a person rearranges (see [[ui-reordering]]). A gap-free sequence; Format turns it into a code string, and scope restarts it per parent row.

stable4 examples compiled by CIcountermodeldata

Summary#

A Counter assigns a sequential number when a row is created — the order number, the invoice code, the ticket number. The platform allocates it, so two requests creating rows at the same instant cannot receive the same value.

This is not the row's identity: every entity already has an Id. A counter is the number a human uses — the one they read down the phone.

Signature#

Counter <Name> = new() {
  Start     = <n>,                    // the seed; the first value assigned is Start + Increment
  Increment = <n>,                    // default 1
  Format    = $"…{Value:D4}…",        // optional — makes the value a formatted CODE string
};

entity <E> {
  [Counter(<Name>)] int <Member>;                  // the raw number
  [Counter(<Name>)] string <Member>;               // the formatted code (needs a Format)
  [Counter(<Name>, scope = <Ref>)] int <Member>;   // restarts per parent row
}

Description#

A plain sequence — 1, 2, 3#

Declare the counter, then tag the member with the [Counter] attribute — [Counter(Name)] naming the counter to draw from. You never assign it — creating the row does, and the compiler enforces it: writing a counter member anywhere (an initializer, an assignment, a bulk Update body) is a compile error. Reading and filtering on it are ordinary:

Counter OrderNumber = new() { Start = 1000 };

entity Order {
  [Counter(OrderNumber)] int Number;
  [Required] string CustomerName;
}

void NewOrder(string customer) {
  var o = new Order { CustomerName = customer };
  // Number is assigned here — 1001 for the first order, then 1002, 1003 …
}

Note the first value is Start + Increment, not Start: Start = 1000 gives you 1001 first. Read Start as "the number already used", not "the number I want first".

A formatted code — INV-0001#

Format is an interpolated string with a Value placeholder, and it turns the sequence into the code your business actually uses. Tag a string member to get it:

Counter InvoiceNumber = new() { Start = 0, Format = $"INV-{Value:D4}" };

entity Invoice {
  [Counter(InvoiceNumber)] string Code;   // "INV-0001", "INV-0002", …
  decimal Total;
}

:D4 is the C# format specifier for "at least 4 digits, zero-padded". Pad generously — a code that jumps from INV-9999 to INV-10000 will sort wrongly in every spreadsheet it ever lands in.

What can go in the template?

Exactly three, each with an optional :format:

holefills with
{Value}the number
{DateTime.UtcNow} · {DateTime.Now}the current instant, UTC — the two spellings fill identically
{DateTime.UtcNow.InZone(<IANA zone>)}that instant read as a zone's wall-clock time

Anything else is a compile error. It used to be copied into the value verbatim, so $"INV-{Vaule:D4}" printed INV-{Vaule:D4} on every row until somebody noticed in the data.

A bare instant hole is UTC, whichever way it is spelled — see Current time (DateTime.UtcNow, DurableClock.Now). {DateTime.Now} once rendered the server's local time here, which is the wrong year for anyone reading the invoice from another zone, and wrong at a boundary nobody tests. Name the zone when the number should carry a local year:

Counter InvoiceNumber = new() { Start = 0, Format = $"INV-{DateTime.UtcNow.InZone(Europe/Stockholm):yyyy}-{Value:D4}" };

The zone id is unquoted, unlike Zone.Of("Europe/Stockholm") in an expression — the template already lives inside a string, so a nested quote would end it. An IANA id has no spaces or brackets, so nothing is ambiguous without them.

Counting by something other than 1#

Counter BatchNumber = new() { Start = 0, Increment = 10 };   // 10, 20, 30 …

entity Batch {
  [Counter(BatchNumber)] int Seq;
  [Required] string Name;
}

A sequence that restarts per parent#

scope restarts the sequence for each distinct value of a reference — so each project's tickets are numbered from 1, which is what people expect when they say "ticket 3 on the Apollo project":

Counter TicketSeq = new() { Start = 0 };

entity Project {
  [Required] string Name;
}

entity Ticket {
  [Required] string Title;
  Project Project;
  [Counter(TicketSeq, scope = Project)] int Seq;   // 1, 2, 3 … within EACH project
}

Without scope the numbers would be global — Apollo's first ticket might be 4,812, because Mercury used the first 4,811. With it, every project starts at 1.

Can I use it as the primary key?#

Do not use a counter as the primary key, and do not use the Id as an order number. They answer different questions: the Id identifies the row to the system, the counter names it to a person. Both exist because both are needed.

See also#

  • entity — the Id every entity already has
  • constraints[Unique], for when the value comes from outside rather than a sequence
  • entity members — the member types a counter can fill

Related

An order the person maintains

When the order of a list is a fact the person owns rather than something a field implies, store it: an int Position on…

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…

constraints

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