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.
holdas the default is rejected: it carries the release-job risk for value thatproposelargely delivers already.- Scheduling constraints as code is rejected: they are exactly the per-client variation ADR-0003 exists to keep in config.