Concepts
Core mental model for Openavail.
The arbitration model
Openavail is a neutral arbitration endpoint. Agents do not write to calendars directly — they ask Openavail first. Openavail applies your rule set, checks the live calendar state, and returns a decision: accept, reject, counter, or preempt.
This means:
- Openavail holds the only OAuth token that can write to the calendar. Agents authenticate with API keys scoped to specific calendar owners.
- Every decision is deterministic and logged. The same request made twice with the same state produces the same decision.
- Conflicts are resolved by priority, not by arrival order.
Holds
A hold is a temporary reservation of a time slot or negotiation window. POST /v1/availability/search only discovers candidates; when an agent is ready to reserve capacity, it calls POST /v1/holds. The hold is not visible on the calendar yet — it is a server-side reservation that prevents other agents from claiming the same capacity while the first agent decides.
Holds expire after ttl_seconds (default 300). If the agent does not confirm, the slot is released automatically.
This pattern — search, hold, then confirm — is what prevents double-booking races while still allowing agents to look without reserving capacity. See Two-phase commit for the full flow.
Meeting classes and priority
Every booking request must include a meeting_class string. Meeting classes are configured in the dashboard and carry two independent fields that are easy to conflate: priority_tier and preempt_policy.
priority_tier — who wins a conflict?
priority_tier determines the outcome when two bookings compete for the same slot. The higher tier wins:
| Tier | Beats |
|---|---|
critical | Everything |
high | normal and low |
normal | low only |
low | Nothing |
If two bookings have the same tier, the one that arrived first wins (by hold creation timestamp).
preempt_policy — what happens to the loser?
preempt_policy is a separate question: once a booking is displaced, what does the engine do with it?
| Policy | What happens to the displaced booking |
|---|---|
protected | It cannot be displaced at all — the incoming request is rejected instead |
reschedulable | It is displaced; the owner's agent gets a booking.displaced notification |
replaceable | It is displaced silently; no notification is sent |
How they work together
The two axes are independent. A booking can be critical priority but reschedulable — it wins almost every fight, but if something does displace it, the agent gets notified. A booking can be low priority and protected — it rarely wins conflicts, but the owner has said "don't touch this one regardless."
The typical pattern for the four default classes:
| Class | priority_tier | preempt_policy | Intent |
|---|---|---|---|
| Critical | critical | protected | Immovable — wins everything and cannot be bumped |
| Important | high | replaceable | High priority; silently yields only to critical |
| Regular | normal | reschedulable | Standard meetings; notified when displaced |
| Placeholder | low | reschedulable | Soft holds; notified when displaced |
Sacred bookings
A committed booking becomes sacred — immune to displacement regardless of priority tier or preempt policy — when any of the following is true:
- Age: the booking was committed more than
sacred_meeting_age_hoursago (default 24 h, configurable per org) - External RSVP: at least one external attendee has accepted the invite on the calendar provider — even if Openavail originally wrote the event
- Human origin: the calendar event was created directly in Google or Outlook (not via an agent), and discovered by the reconciler
Any one of these signals makes the booking sacred. An agent attempt to preempt a sacred booking returns Reject(reason: SACRED_MEETING) regardless of the incoming request's priority.
Rule layers
Rules cascade from org to individual:
Org defaults → calendar owner overrides
The calendar owner's rules win on any field they explicitly set; otherwise they inherit the org default. Rules control:
- Working hours and blackout windows
- Buffer time between meetings
- Maximum daily meeting counts
- Per-class priority boosts
See Rule layers for the full configuration reference.
Audit rows
Every decision Openavail makes — accept, reject, preempt, counter — produces an audit row. The row records:
| Field | What it captures |
|---|---|
decision_id | Unique identifier (dec_…) |
action | accept, reject, preempt, counter |
actor.agent_id | Which registered agent made the request |
actor.api_key | Which specific key (for revocation tracing) |
owner | The calendar owner whose time was affected |
rule_fired | Which rule produced the decision |
latency_ms | P99 end-to-end latency |
Audit rows are immutable. They cannot be deleted — they can only be exported or expire per your retention policy.
Booking responses do not include an audit_row_id. To link a booking to its audit row, use the correlation_id returned on every booking write response — it is stamped on the corresponding audit row and can be used to look up the decision in the audit log.
Google account types
How Openavail treats a connected Google account depends on whether it is a personal account or a Google Workspace account. The distinction is determined automatically at sign-in — you do not configure it manually.
| Personal Google account | Google Workspace account | |
|---|---|---|
| How to identify | @gmail.com or any address without a Workspace tenant | Corporate or edu address backed by a Workspace tenant |
| Plan assigned | Individual | Team |
| Domain deduplication | Not applied | Teammates signing in with the same domain are provisioned as members of the same workspace |
| JIT provisioning | No — each sign-in creates a separate workspace | Yes — the first user from a domain creates the workspace; subsequent users from the same domain join it |
Why it matters for agents: Agents identify calendar owners by email (owner_email in API requests). The email is stable regardless of account type or Google Workspace setup. The account type only affects how the dashboard is organised.
Multiple calendars and the primary calendar
A calendar owner can connect more than one calendar (for example, a work Google calendar and a personal one). When an agent calls POST /v1/availability/search, Openavail:
- Unions free/busy data from all of the owner's connected calendars to give an accurate availability picture.
- Writes the hold and any resulting booking to the owner's designated primary calendar.
The owner sets their primary calendar in the dashboard (Dashboard → Calendars → Set as primary). If the primary is disconnected, agents receive CALENDAR_NOT_FOUND until a new primary is set.
To target a specific calendar type, agents can pass "calendar_type": "work" (or "personal" / "other") alongside owner_email. Openavail resolves to the matching calendar and falls back to the primary if no match exists. The resolved_calendar_type field in the response confirms which calendar was selected.
calendar_type: null is the default state. A calendar has no type label until the owner sets one in the dashboard (Dashboard → Calendars → edit the calendar). Agents should treat null as "untagged primary" rather than an error — availability checks and bookings still work normally. If an agent passes "calendar_type": "work" but the owner has no calendar tagged as work, Openavail falls back silently to the primary. To avoid this ambiguity, ask owners to tag their calendars during onboarding.
Timezones
All datetimes in the Openavail API are UTC. Every start, end, earliest_start, and latest_end field must be an ISO 8601 string in UTC (e.g. 2026-07-01T14:00:00Z). The API never accepts or returns timezone-offset times.
Agents are responsible for converting to UTC before calling. When a user says "book something at 2pm", the agent needs the calendar owner's timezone to produce the correct UTC time.
Getting the owner's timezone: call GET /v1/calendar-owners/:owner_email/calendars or GET /v1/owner-context before booking. Each calendar in the response includes a timezone field (IANA identifier, e.g. "Europe/Berlin"). Use the primary calendar's timezone — is_primary: true — as the owner's local timezone. The value is populated automatically when the owner connects their calendar; null is returned only if the provider did not supply one.
{
"calendars": [
{
"calendar_type": "work",
"is_primary": true,
"timezone": "Europe/Berlin"
}
]
}
A safe agent pattern: fetch the owner's calendars once at session start, read timezone from the primary, apply that offset for all UTC conversions in the session. Fall back to asking the user if timezone is null.