Skip to content

ADR-0035: Scheduling capability model

Context

A reply to "can you come out and look at the switchboard?" is far more useful if it offers a time. But offering times means touching the system a client's whole day runs on, and the systems differ enormously in what they expose.

Two distinct products hide behind the word "scheduling":

Who is hurt when it goes wrong
Assistance The reply mentions availability. A human still books The approver catches it
Automation We hold or book slots, changing their calendar Their business, and nobody notices for days

A bad draft is caught at approval. A phantom hold is caught when a week looks fully booked and is not.

Designing against one vendor's API would also be wrong: the same question has to be answerable by AroFlo, Simpro, ServiceM8 or a shared Outlook calendar, and their models of a "slot" have nothing in common.

Decision

1. Capabilities are declared by the connector, not assumed

scheduling
├── find_slots            can it answer an availability question at all?
│   ├── per_resource      are slots attributable to a person?
│   └── with_skills       are skills attached to resources?
├── tentative_hold        is there a real provisional state?
└── book                  can a booking be created?

A system that lacks a capability simply does not offer it, and the product degrades rather than faking it.

Never fake a hold

A system without a genuine tentative state must not be given one by creating a real job and deleting it later. That pollutes their schedule, their reporting and possibly their job numbering, and it is worse than offering no times at all.

2. We ask a question. We do not read a calendar

find_slots(
  duration,
  skills_required,   ← our taxonomy
  location,
  window,
  constraints        ← tenant rules
) → [ slot ]

"Read the calendar" is not portable and is not even meaningful: a quote needs a salesperson, a job needs someone qualified for it, and a shared calendar answers neither question.

Each connector answers however its system allows. A system with per-resource availability and skills answers precisely. One with a single shared calendar answers loosely, and says so.

3. Three things are ours, not the source system's

Why
Skills taxonomy Every system labels them differently. We map theirs to ours, exactly as with the canonical job (ADR-0006)
The constraint language Tenant rules expressed once, not per connector
The slot shape Start, end, resource, and confidence

Same principle as the canonical model: no source vocabulary leaks in, so replacing the source system is a connector change.

4. Constraints are structured predicates, not scheduling logic

Tenant scheduling rules use the same {fact, operator, value} shape as everything else (ADR-0004). No expression language, no eval.

Rule Kind
Never before 7am, never after 4pm Hours
Nothing on a Friday afternoon Day
Two hours travel buffer for regional Location-derived
Only Dave for switchboards Skill to resource
Nothing within 24 hours Lead time

These are config, not code (ADR-0003), so a client's scheduling preferences never require a deploy.

5. scheduling_mode, per tenant, bounded by capability

Mode Requires The draft says
none Nothing "Someone will call to arrange a time"
propose find_slots "Thursday or Friday morning look open, subject to availability"
hold tentative_hold "I have pencilled you in for Thursday 9am, held until Monday"

Default is propose, falling back to none where the connector cannot answer. A graceful degrade, not an error.

Selecting a mode the connector cannot support is refused by the Console, never silently downgraded. Same principle as the approval floors: config may not claim a capability that does not exist.

6. The draft may only claim what the answer supports

Slots go into the Draft prompt's KNOWN block, so proposing a time is not invention.

Connector answered The draft may say
Slots with a named, skilled resource "Dave is free Thursday morning"
Slots, no resource attribution "Thursday morning looks open", nothing about who
Nothing Times sit in NOT KNOWN. No time is offered

Claiming more than the answer supports is the exact failure the KNOWN and NOT KNOWN structure exists to prevent.

7. hold is off by default, and the release job is the risk

Hold expiry is tenant config, typically two to three days.

The release mechanism is a scheduled job that must run reliably forever. If it fails silently, their calendar fills with phantom holds and nobody notices until a week looks fully booked and is not. That destroys trust faster than a bad draft ever could.

So hold requires: a genuine tentative_hold capability, a proven release job, and a client who asked for it.

Consequences

propose delivers most of the value for a read-only integration and no scheduling risk, which is why it is the default. It removes the "when suits you" round trip, which is one of the most common wasted exchanges in a service inbox.

Because capabilities are declared, a weaker source system produces a vaguer draft rather than a wrong one, and nobody has to remember which client is on which system.

The costs are real: a skills taxonomy to maintain alongside the canonical model, a constraint vocabulary to define, and a find_slots implementation per connector that is genuinely different each time rather than a thin wrapper.

Proposed times go stale faster than the rest of a draft. A draft written Monday offering Thursday may be offering a slot that filled on Tuesday. "Subject to availability" covers the wording, and tenants on propose want a shorter draft_staleness. Re-checking availability at approval time would be better than regenerating, and is not yet designed.

Alternatives considered

  • Read the calendar directly is rejected: not portable, and it answers the wrong question. Availability without skills or resources tells you nothing about whether the work can actually be done.
  • Design against AroFlo and generalise later is rejected: the same reasoning as connectors-per-source. The abstraction is cheaper to define now than to retrofit around one vendor's model.
  • Hold by creating and deleting real jobs is rejected outright. It pollutes the client's operational data to simulate a state their system does not have.
  • hold as the default is rejected: it carries the release-job risk for value that propose largely delivers already.
  • Scheduling constraints as code is rejected: they are exactly the per-client variation ADR-0003 exists to keep in config.