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.
Windowsare recurring weekly intervals in local time. The parent'sZonegives them a DST-correct instant (a09:00Stockholm 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.Zoneis 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.Exceptionsare date-range overrides applied on top of the windows. AnIsClosedexception (a public holiday) removes accrual across itsFrom..To; theReasonis for humans.DayOfWeekis the C# built-in enum, sow.Day == DayOfWeek.Mondayreads 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> —
Accruesnames the states in which the clock runs - subscribe — the slot whose promise the clock measures