Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne program

Reference / Workflow

ServiceHours (SLA-accrual windows)

entity ServiceHours { string Name; Zone Zone; ServiceWindow[] Windows; ServiceException[] Exceptions; }

A schedule the SLA clock accrues within — the platform WALKS its weekly windows (and holiday exceptions) to advance ticks and compute deadlines. The platform defines the shape; the app fills the rows (BusinessHours, AroundTheClock) and assigns which one governs an instance. No windows ⇒ the identity schedule (ticks == wall-clock).

stable1 example compiled by CIworkflowtype

Summary#

ServiceHours is the window an SLA clock accrues within. The platform's tick-accrual engine walks a schedule's weekly Windows (and any holiday Exceptions) to advance SLA time and compute deadlines — so a ticket on a business-hours schedule simply doesn't burn its clock at night, while one on a 24/7 schedule accrues around the clock. The shape is platform (the engine must walk it), the rows are the app's (it seeds BusinessHours / AroundTheClock, assigns which one governs an instance, and owns everything about the SLA's meaning).

Signature#

public entity ServiceHours {
  string Name;
  Zone   Zone;                    // REQUIRED — the zone the windows' local times are read in (DST-correct)
  [ForeignKey(ServiceHours)] ServiceWindow[]    Windows;     // recurring weekly open windows — Mon 09:00–17:00, …
  [ForeignKey(ServiceHours)] ServiceException[] Exceptions;  // holiday / special-hours date ranges
}

public entity ServiceWindow {
  ServiceHours ServiceHours;
  DayOfWeek Day;                  // the C# built-in enum (Sunday = 0 … Saturday = 6)
  TimeSpan  Start;  TimeSpan End; // local time-of-day, as a TimeSpan from midnight
}

public entity ServiceException {
  ServiceHours ServiceHours;
  DateTime From;  DateTime To;
  bool     IsClosed;              // the clock does not accrue across From..To
  string   Reason;
}

Description#

ServiceHours is ordinary app data (a data-DB entity), editable through the app's own UI — not a declaration block. An app seeds the schedules it needs and points an instance at one (typically snapshotted in the workflow's Start { } from a contract/severity matrix). The platform reads the rows and walks them; it never defines them.

  • Windows are recurring weekly intervals in local time. The parent's Zone gives them a DST-correct instant (a 09:00 Stockholm window is a different UTC instant in summer and winter). A schedule with no windows is the identity schedule — ticks equal wall-clock — which is the 24/7 / "AroundTheClock" case.
  • Zone is required. An SLA schedule must always state the zone its hours are measured in, so a row can never be saved ambiguous — the platform rejects it at commit (UI edit or seed), not just at compile. A 24/7 schedule names a zone too (Zone = "UTC"); it's inert there (with no windows the walk never reads it) but keeps every schedule self-documenting.
  • Exceptions are date-range overrides applied on top of the windows. An IsClosed exception (a public holiday) removes accrual across its From..To; the Reason is for humans.
  • DayOfWeek is the C# built-in enum, so w.Day == DayOfWeek.Monday reads and stores exactly as in C#.

Binding it to a workflow. A workflow names the schedule its clocks accrue within with a ServiceHours = <expr>; setting (a value over this.Item resolving to a ServiceHours ref, typically snapshotted onto the entity in Start). Every SLA clock on the run — a state's Expire, a milestone's Within, a reminder — then advances only inside that schedule's windows, so a deadline computed from a 4-hour budget lands after the intervening nights and weekends, not 4 wall-clock hours later. With no ServiceHours binding a run uses the identity schedule (24/7).

workflow SupportTicket {
  ServiceHours = this.Item.ServiceHours;   // the schedule this run's clocks walk
  Accrues      = [Open, Working];          // and the states in which they run at all
  ...
}

The binding LOOKS UP a schedule; it must not create one. ServiceHours = <expr>; is a setting, and a setting is re-evaluated on every clock arm, every state entry and every inbox read. On a read path the engine's work is never committed, so a create-if-missing bound here writes rows that are thrown away and hands back a row that does not exist — leaving the app with an empty inbox and no error. The compiler refuses it, naming the function that writes; seed the schedule once, from a function you call yourself, and let the setting only look it up. The same rule covers every expression-valued workflow setting (Autostart, Deadline, Expire, Within, Backoff, When, Assignee, Reassign, CompleteWhen, a route arm's guard) — reading and calling are fine there; only writing is refused.

void SeedSchedules() {                                   // called from the app's own setup, ONCE
  if (ServiceHours.Any(h => h.Name == "Business hours")) { return; }
  var hours = new ServiceHours { Name = "Business hours", Zone = Zone.Of("Europe/Stockholm") };
  new ServiceWindow { ServiceHours = hours, Day = DayOfWeek.Monday, Start = TimeSpan.FromHours(9), End = TimeSpan.FromHours(17) };
  UnitOfWork.Commit();
}

ServiceHours BusinessHours() { return ServiceHours.Single(h => h.Name == "Business hours"); }

workflow SupportTicket {
  ServiceHours = BusinessHours();          // ✓ a lookup
// ServiceHours = SeedAndReturnHours();    // ✗ refused — a setting that writes
  ...
}

One schedule per tenant#

ServiceHours carries no tenant column, and a partial entity ServiceHours { Organization? Org; } is refused — a partial states security, it does not reshape a platform table. The reference goes the other way round, on your own tenant entity, which you do own and can shape freely. Seed one schedule per tenant, point each tenant's row at its own, and read it through the run:

entity Organization {
  [Required, MaxLength(80)] string Name;
  ServiceHours? Hours;                       // ← the FK lives on YOUR entity
  security { allow read, create, update when IsAuthenticated; }
}

workflow SupportTicket {
  ServiceHours = this.Item.Org.Hours;        // ← each run walks its own tenant's calendar
  ...
}

Every run then accrues in its own tenant's windows and zone — a Stockholm customer's four-hour SLA and a New York customer's land at different instants, from one workflow declaration.

Two schedule-shaped things, deliberately split. ServiceHours is when the SLA clock accrues — the customer's promise window. It is not availability (who is on shift): rosters, leave, and follow-the-sun live entirely in the app and the platform never consults them. The promise (ServiceHours) and the people (availability) are different concerns with different owners.

Examples#

enum OrderState { Open, Done }

[Principal]
entity Person {
  [Required, MaxLength(200)] string Email;
  security { allow read, create when IsAuthenticated; }
}

entity Order {
  [Required, MaxLength(60)] string Reference;
  OrderState Status;                       // no default: the workflow owns this field
  security { allow read, create, update when IsAuthenticated; }
}

// The schedule entities ship with the platform; an app that reaches them says who may.
partial entity ServiceHours   { security { allow read, create when IsAuthenticated; } }
partial entity ServiceWindow  { security { allow read, create when IsAuthenticated; } }

// Two schedules: one that accrues around the clock, one that only counts business hours.
void SeedSchedules() {
  new ServiceHours { Name = "AroundTheClock", Zone = Zone.Of("UTC") };          // no windows ⇒ 24/7

  var biz = new ServiceHours { Name = "BusinessHours", Zone = Zone.Of("Europe/Stockholm") };
  new ServiceWindow { ServiceHours = biz, Day = DayOfWeek.Monday,
                      Start = TimeSpan.FromHours(9), End = TimeSpan.FromHours(17) };
}

Seed a 24/7 schedule and a Mon–Fri business-hours one with a holiday:

var aroundTheClock = new ServiceHours { Name = "AroundTheClock", Zone = "UTC" };   // no windows ⇒ accrues 24/7
var bizHours = new ServiceHours { Name = "BusinessHours", Zone = "Europe/Stockholm" };
foreach (var d in [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday]) {
  new ServiceWindow { ServiceHours = bizHours, Day = d, Start = TimeSpan.FromHours(9), End = TimeSpan.FromHours(17) };
}
new ServiceException { ServiceHours = bizHours, From = new DateTime(2026, 6, 19), To = new DateTime(2026, 6, 19),
                       IsClosed = true, Reason = "Midsommarafton" };

See also#

  • Assigned / Finished (milestones) — the SLA clock whose ticks accrue within these windows
  • <span class="planned" title="this page is planned and not written yet">workflow-state</span> — Accrues names the states in which the clock runs
  • subscribe — the slot whose promise the clock measures

Related

Assigned / Finished (milestones)

A milestone puts an SLA on a slot's progress — Assigned (someone must PICK IT UP within Within) and Finished (it must…

subscribe

Declares that a workflow state waits on an event, and configures the wait — who may hold it, who may hand it on…