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:

TierBeats
criticalEverything
highnormal and low
normallow only
lowNothing

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?

PolicyWhat happens to the displaced booking
protectedIt cannot be displaced at all — the incoming request is rejected instead
reschedulableIt is displaced; the owner's agent gets a booking.displaced notification
replaceableIt 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:

Classpriority_tierpreempt_policyIntent
CriticalcriticalprotectedImmovable — wins everything and cannot be bumped
ImportanthighreplaceableHigh priority; silently yields only to critical
RegularnormalreschedulableStandard meetings; notified when displaced
PlaceholderlowreschedulableSoft 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_hours ago (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:

FieldWhat it captures
decision_idUnique identifier (dec_…)
actionaccept, reject, preempt, counter
actor.agent_idWhich registered agent made the request
actor.api_keyWhich specific key (for revocation tracing)
ownerThe calendar owner whose time was affected
rule_firedWhich rule produced the decision
latency_msP99 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 accountGoogle Workspace account
How to identify@gmail.com or any address without a Workspace tenantCorporate or edu address backed by a Workspace tenant
Plan assignedIndividualTeam
Domain deduplicationNot appliedTeammates signing in with the same domain are provisioned as members of the same workspace
JIT provisioningNo — each sign-in creates a separate workspaceYes — 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:

  1. Unions free/busy data from all of the owner's connected calendars to give an accurate availability picture.
  2. 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.