# ServiceHours (SLA-accrual windows)

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

<!-- id: workflow-service-hours · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/service-hours/ -->

## Summary        {#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      {#signature}
```osy
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    {#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).

```osy title="binding a schedule to a workflow" syntax
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.

```osy title="seed once; the setting only looks it up" syntax
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   {#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:

```osy title="two organisations, two schedules, two zones" syntax
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       {#examples}
```osy title="a schedule the engine's clocks accrue against" test app=workflow-service-hours
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:

```osy title="seed two schedules" syntax
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       {#see-also}
- [Assigned / Finished (milestones)](https://osysharp.com/reference/workflow/milestone/) — 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](https://osysharp.com/reference/workflow/subscribe/) — the slot whose promise the clock measures
