Skip to main content

Qontak | Chatbot | AI Agent — Qontak Action — Appointment Booking Actions

HEADER BLOCK

FieldValue
PMDimas Fauzi Hidayat
PRD Version1.1
StatusDRAFT
PRD TypePHASE
EpicTBD — create once RFC is agreed (child Epic per the initiative's umbrella model)
SquadHadiningbot Squad
Product ModuleBot, AI, and Automation
RFC LinkN/A — pending
Figma MasterN/A — pending. No qontak-designer prototype exists for booking actions (design census 2026-07-22: no appointment/booking screens anywhere) — tracked as a design dependency in Section 15.
AnchorQontak Action — ANCHOR
Labelsepic:qontak-chatbot | module:ai-agent | feature:appointment-booking-actions
Last Updated2026-07-25

Status values: DRAFTREADYBUILDSHIPPED READY gate: Epic cannot move to In Progress in Jira without PRD Link + RFC Link populated.

Table of Contents

2. CONDITIONAL BLOCK: PHASE CONTEXT

Anchor PRD: Qontak | Chatbot | AI Agent — Qontak Action — ANCHOR
(../qontak-action-anchor.md)
Initiative: Qontak Action — first-party actions in Qontak's own features, invoked via
function-calling, authenticated with the company's Qontak token. No
cross-product credential / HMAC / OAuth approval required.
Catalog item: Appointment Booking actions — create_booking & find_booking (two actions
in the Qontak Action catalog, not a sequential phase).
Phase Goal: Enable a Qontak chatbot builder to give their AI Agent native
"Create Booking" and "Find Booking" actions against the Qontak
Appointment service, so the agent can autonomously book an appointment
(service + date + slot + customer details) and look up existing bookings
mid-conversation — instead of handing the customer to a human or an
external booking link.

Sibling actions: Qontak CRM actions, Qontak Chat actions, Send Attachment, Send WhatsApp
Flow — same initiative, same action framework, company token.

This action: TWO native AI Agent actions targeting the qontak-appointment service:
• qontak_booking_create → POST /appointments/bookings (idempotent,
X-Source-Id: ai; staff omitted → the appointment service's round-robin
assigns fairly).
• qontak_booking_find → GET /appointments/bookings (filters: customer
phone / name search / date range / status; compact response for the
agent's context).
Both reuse the existing action framework end-to-end: NodeRegistry rows
(schema), a new mekari_qontak_booking executor group (dispatch), company-
token auth via CredentialResolver, sync to AI Service as
qontak_function_call tools. The appointment service ALREADY whitelists
AI callers server-side (aiAllowedActions) — create, find, slots, and
catalog lookups are permitted; reassign/relocate/resolve-conflicts and
completed/no-show status are server-blocked for AI.

Deferred to next: Reschedule and cancel actions (both AI-whitelisted server-side already —
deferred to keep v1 scope tight). Multi-location routing UX beyond a
builder-pinned default location. Payment/deposit collection at booking
time. WhatsApp Flow form-based capture of booking fields (pairs naturally
with the Send WhatsApp Flow action later).

Cross deps: BLOCKING — qontak-appointment must accept the company token for
service-to-service calls (today its authMiddleware only accepts a user
session Bearer validated against Qontak SSO). Squad-resolvable inside
the appointment repo (no DSAI dependency anywhere in this phase — the
AI-service sync layer is generic and untouched). OPS — NodeRegistry rows
are seeded via RailsAdmin, not code.

3. One-liner + Problem

One-liner: Give the autonomous AI Agent native Create Booking and Find Booking actions against Qontak Appointment, so appointment-led businesses convert conversations into confirmed bookings without human handoff.

Problem: Appointment-led businesses (clinics, education providers, salons, consultants) run their sales motion on WhatsApp, and the conversation's natural conclusion is a booked slot — but the AI Agent has no way to act on the Qontak Appointment service. Today the agent can qualify a lead and discuss availability in free text, then must hand off to a human admin who re-reads the thread and books manually, or paste an external booking link that breaks the conversational flow and loses conversions. The EAP discovery interview with Cerdas Lab Indonesia (education services, Jabodetabek) documents exactly this: the lead-to-appointment workflow is the core POC ask, scheduling is constraint-heavy, and manual booking admin consumes sales capacity (source). Meanwhile the receiving side is already built: the qontak-appointment API whitelists AI callers (X-Source-Id: ai) for booking create/read, slot lookup, and catalog reads, with idempotency and fair round-robin staff assignment — the capability exists and is unreachable from the agent.

Cost of inaction: every AI-contained conversation that needs a booking still costs a human touch (the exact containment the AI Agent is sold on), and appointment-led EAP prospects evaluate competitors whose bots book natively. From Oct 1 2026, Meta charges for service messages — every extra human-handoff round-trip on WhatsApp also becomes a direct cost.

4. Target Users + Persona Context

PersonaRoleGoalPainWorkaround
Primary — CS Ops Lead / Bot ManagerOwns the AI Agent configuration for an appointment-led businessThe agent books qualified customers into real slots end-to-endAgent stops at "our admin will contact you" — booking requires a human on the appointment dashboardHuman admin re-reads the WhatsApp thread and books manually in the Appointment web app; or an external booking-page link is pasted into chat
Secondary — End customer (on WhatsApp)Prospect/customer chatting with the AI AgentPick a service and time and get a confirmed booking in the same conversationRedirected to a link or told to wait for a callback; drop-off before bookingCalls the business or books elsewhere
Secondary — Human agent / adminHandles handoffs and manages the appointment calendarOnly handle bookings that genuinely need judgment (conflicts, special requests)Every AI conversation that reaches "book me" lands in their queue as manual data re-entryCopy-pastes customer name/phone/slot from chat into the Appointment app

5. Non-Goals

  1. No reschedule or cancel actions in v1. Both are AI-whitelisted server-side already; deferred to a follow-up catalog item to keep this phase shippable.
  2. No AI access to reassign / relocate / resolve-conflicts. The appointment service blocks these for X-Source-Id: ai callers by design; this PRD does not ask to widen the allowlist.
  3. No AI-set completed / no-show statuses. Server-blocked for AI callers; attendance truth stays human-owned.
  4. No changes to the Qontak Appointment product itself (no new booking rules, no scheduling-engine changes, no appointment UI work) — except the service-to-service auth acceptance listed as a dependency.
  5. No new staff-selection logic. When the customer has no staff preference the action omits staff_id and the appointment service's existing round-robin assigns fairly; this PRD does not build agent-side staff ranking.
  6. No payment/deposit collection at booking time.
  7. No booking-data storage in the chatbot backend. Bookings live solely in the appointment service; the chatbot keeps only its standard action event logs.
  8. No WhatsApp Flow form capture of booking fields in v1 — the agent collects fields conversationally; pairing with the Send WhatsApp Flow action is a later combination.

Scope Changes

Engineering surfaces this PRD touches (controlled vocab). Kept in sync with the scope_changes frontmatter above.

  • Backendchatbot: new mekari_qontak_booking executor group (node_type_registry.rb GROUP entry, nodes/mekari_qontak_booking/execute.rb with ACTION_TYPE_MAPPING for qontak_booking_create/qontak_booking_find), new booking_http_client.rb (company-token auth via CredentialResolver, sends X-Source-Id: ai + Idempotency-Key), new node_resources/mekari_qontak_booking/lookup_resources.rb (service/staff/location dropdowns); NodeRegistry + AiAgentTool rows (RailsAdmin/ops). qontak-appointment: accept company-token auth for service-to-service calls and map org → tenant (external_company_id) — the single blocking dependency (Section 15).
  • Frontendchatbot-fe: ACTION_LIST entries for the two booking node types (ai-agent-actions-constants.ts), isBookingNode predicate + visibility gate (useActionFormVisibility.ts), endpoint-flag-map rows for booking resource-lookup endpoints, feature-flag entry (rollout_ai_agent_booking_action).
  • Design — Figma for the booking-action config drawer (service/location/staff pickers). No qontak-designer prototype exists yet (TBD — Section 15).

6. Constraints

#ConstraintDetail
6.1Feature flagrollout_ai_agent_booking_action | default: OFF (system_preferences rollout-group pattern, same family as rollout_ai_agent_action). Flag OFF → the two booking actions are not rendered in the FE action picker and not registered as callable tools on train.
6.2Plan / eligibilityOrg must have BOTH the AI Agent (autonomous, V2 capability engine) and an active Qontak Appointment tenant. Orgs without an appointment tenant never see the actions (FE visibility gate + BE lookup failure).
6.3PlatformConfig: web only (AI Agent config UI). Runtime: channel-agnostic — the actions run wherever the autonomous agent runs; primary launch channel is WhatsApp.
6.4AuthCompany token via OrganizationConnection resolved through CredentialResolver (managed credential), refreshed on 401 via the existing company-token SSO endpoint pattern. NEVER a per-user session token. All calls carry X-Source-Id: ai so the appointment service enforces its own AI allowlist server-side (defense in depth — the chatbot side never relies on client-side restraint).
6.5IdempotencyEvery qontak_booking_create call sends an Idempotency-Key derived from (room_id, action invocation id) so an AI retry of the same turn can never double-book. Appointment service replays return the existing booking (200) instead of creating a duplicate (201).
6.6PerformanceAction execution is synchronous inside the agent's reasoning loop: p95 executor round-trip (chatbot BE → appointment API → response) ≤ 3s; resource-lookup endpoints (services/staff/locations) p95 ≤ 2s; find_booking responses pruned via response_fields to keep agent context compact (≤ 10 bookings per call, newest first).
6.7Server-side AI restrictions (inherited, not built)The appointment service's aiAllowedActions allowlist governs: AI may create bookings, read bookings/slots/services/staff/locations, and set limited statuses; AI may NOT reassign, relocate, resolve conflicts, or set completed/no-show. This PRD treats the allowlist as an immutable contract.
6.8Data lifecycleNo new persisted artifacts in the chatbot BE: booking data lives solely in the appointment service (its own retention rules); the chatbot records only the standard per-invocation action event log (existing enqueue_event_log path, existing retention). The Idempotency-Key lives in the appointment service's booking_idempotency table with its existing 24h replay window — no cleanup work in this PRD.

7. Feature Changes

Change IDChange TypeSurfaceBeforeAfter
CHG-001ADDchatbot BE — node_type_registry.rb GROUP5 executor groups (api, mekari_qontak_crm, mekari_qontak_chat, google_sheets, google_calendar)+mekari_qontak_bookingMekariQontakBooking::Execute, with ACTION_TYPE_MAPPING: qontak_booking_createPOST /appointments/bookings, qontak_booking_findGET /appointments/bookings
CHG-002ADDchatbot BE — NodeRegistry + AiAgentTool rowsNo booking node types existTwo NodeRegistry rows (param schemas incl. destination, use_ai, is_rl resource lookups, depends_on) + AiAgentTool rows so SyncToAiService emits them as qontak_function_call tools. Generic layers (action_execute.rb, sync_to_ai_service.rb, process_async_reply.rb) are unchanged.
CHG-003ADDchatbot-fe — action pickerACTION_LIST has CRM/api entries only; no booking predicate in useActionFormVisibility.ts+2 ACTION_LIST entries; +isBookingNode predicate & visibility computed (flag + appointment-tenant gate) wired into filterActionsByVisibility; endpoint-flag-map rows for booking lookups
CHG-004MODIFYqontak-appointment BE — auth middlewareOnly user-session Bearer tokens (Qontak SSO) accepted; no service-to-service pathCompany token accepted and resolved to the org's tenant via external_company_id (dependency DEP-1, Section 15). Existing user-session auth unchanged.

Everything else in both products — CRM actions, chat actions, the appointment product's booking rules, round-robin, conflict handling — stays exactly as it is.

8. New Features

New config drawer content: "Create Booking" and "Find Booking" actions (same drawer family as the existing CRM actions — reuse the grouped Standard picker pattern; no new page, per the existing-IA rule).

AI Agent config → Actions → Add action → Mekari Qontak group
├── Create Booking (qontak_booking_create)
│ ├── Action name (text, required)
│ ├── Trigger description (textarea, required) — "when to use", synced to the LLM
│ ├── Location (resource lookup — locations list; single-select, required)
│ │ └── default pre-selected when org has exactly one active location
│ ├── Service (resource lookup — services for the selected location; per-param
│ │ mode: fixed by builder OR AI-filled from conversation)
│ ├── Date / Start (AI-filled — the agent must call slot availability first; see S02)
│ ├── Customer name / phone (AI-filled from conversation; phone E.164)
│ └── Staff — NOT exposed in v1 (omitted → service-side round-robin)
└── Find Booking (qontak_booking_find)
├── Action name + Trigger description (as above)
└── Search params (all AI-filled): customer phone OR name query, optional
date-from/date-to, optional status
URL path: existing AI Agent config route (no new route)

UI states: Loading — location/service lookup fetching; Empty — org has no active appointment tenant → picker hidden entirely (visibility gate), org has tenant but zero active services → drawer shows "publish a service in Appointment first" prompt with Save blocked; Error — lookup fetch failure with retry; Success — required fields valid → Save enabled.

Wireframes (proposed — grounded in the existing qontak-designer action-drawer prototype; designer to formalize in Figma, DEP-3)

All three screens reuse the existing action-drawer chrome (ActionConfigBase: back-arrow header, Action name + trigger fields, grey Field-configurations box, Cancel/Save footer, discard-changes guard). The dashed-blue regions are the only net-new UI. Mode chips follow the existing fixed-vs-AI pattern ("Set manually" / "Let AI decide") from the Create deal drawer; the Find Booking layout follows the Get deal information read-action pattern (add-field picker + ≥1-field validation).

Entry point — two new rows in the existing Add-action picker's Mekari Qontak group; no new page or drawer shell:

Add-action picker wireframe: the existing Create action drawer over the AI Agent Actions tab, with the Mekari Qontak group gaining two new dashed rows — Create booking and Find booking — below Create deal, Create ticket, Update ticket, and Send attachment; a note marks the visibility gate (flag ON + active appointment tenant).

Create Booking config drawer — every configurable field (APPTACT-S01 data fields):

Create booking drawer wireframe: Action name and trigger fields from the shared chrome, then a dashed field-configurations box with Location (builder-pinned select, "Set manually (fixed)", pre-selected for single-location orgs), Service (select, "Let AI decide" default with manual option, scoped to the location), Date and Start time (AI-locked, slot-check note), Customer name and Customer phone (AI-locked with {customer_name}/{customer_phone} variable pills); notes state no Staff field in v1 (round-robin) and the zero-services empty state blocking Save.

Find Booking config drawer — search params are runtime/AI-filled; the builder only curates what information returns:

Find booking drawer wireframe: Action name and trigger fields, then a dashed field-configurations box split into SEARCH BY (Customer phone/name one-of-required, Date range, Status — all locked to "Let AI decide") and INFORMATION TO RETURN (Booking ID and Date-and-time rows with remove buttons, plus an Add field picker offering Service, Status, Staff, Location; response capped at 10, newest first); notes state the at-least-1-return-field validation and that reschedule/cancel are not offered.

Open design gaps for the Figma pass (DEP-3): ① the AI-locked rows (Date, Start time, Customer name/phone, and all Find search params) need a locked variant of the existing mode chip — the current ActionFieldRow always offers both modes; ② multi-location orgs — one action instance per location vs. an AI-selectable location (Open Question 2); ③ whether Find Booking's SEARCH BY rows render as locked field rows (as drawn) or collapse into a read-only info note.

📊 UI State Diagram — Booking action config drawer

stateDiagram-v2
[*] --> Loading: Builder opens "Create Booking" drawer
Loading --> Ready: locations + services fetched
Loading --> Empty: appointment tenant has no active services
Loading --> Error: lookup fetch fails / times out
Error --> Loading: Builder clicks Retry
Empty --> [*]: Builder goes to Appointment app to add services
Ready --> Saved: required fields valid, builder clicks Save
Ready --> Ready: builder edits fields (validation inline)
Saved --> [*]: action listed; registered as tool on next train

9. API & Webhook Behavior

#BehaviorEntity AffectedTriggered ByExpected BehaviorFailure Behavior
1Save booking action configAiAgentAction / agent parameters.capabilities[].actions[], AiAgentToolBuilder clicks Save in the config drawerPersist the action (trigger description, pinned location, per-param fixed-vs-AI choices). On next agent train/sync, SyncToAiService emits it as a qontak_function_call tool with the AI-filled params as the LLM-visible arg schema.• No location selected (multi-location org) → block save.
• Org's appointment tenant unreachable during lookup → drawer error state, Save blocked.
2Agent invokes Create Booking at runtimeA bookings row in the appointment service (via POST /appointments/bookings)AI Agent function-calls qontak_booking_create with AI-filled args after confirming slot availability with the customerExecutor buckets params by destination, calls the appointment API with company token + X-Source-Id: ai + Idempotency-Key; response (booking id, confirmed slot, assigned staff) returns synchronously into the agent's reasoning loop; agent confirms to the customer. staff_id omitted → appointment round-robin assigns.• Slot taken between check and create (409/validation) → structured error returned to the agent → agent re-checks slots and offers alternatives; log ai_agent_booking_create_failed reason slot_conflict.
• Auth failure → one company-token refresh + retry; still failing → log reason auth_failed, agent falls back to handoff.
• Timeout (> Section 6.6) → log reason timeout, agent apologizes + offers handoff; the sent Idempotency-Key guarantees a retry can't double-book.
3Agent invokes Find Booking at runtimeNone (read-only GET /appointments/bookings)AI Agent function-calls qontak_booking_find (customer phone / name / date range / status)Executor queries the appointment list API, prunes via response_fields to a compact list (id, service, date, start, status, staff — max 10, newest first) returned into agent context; agent answers ("your booking is Tuesday 14:00 with …").• Zero matches → empty list returned; the agent tells the customer no booking was found and offers to create one.
• Auth/timeout → same handling as row 2.
4Slot / catalog resource lookupsNone (read-only)(a) Builder opens the config drawer — services/staff/locations lists; (b) Agent checks availability mid-conversation before creatinglookup_resources.rb proxies the appointment read endpoints (/appointments/services, /appointments/locations, /appointments/slots) — all already AI-whitelisted. Slot lookup exposed to the agent as part of the Create Booking action contract (the tool description instructs: check slots before creating).• Lookup failure at config time → drawer error state.
• Slot lookup failure at runtime → agent does not guess availability; offers handoff; log ai_agent_booking_lookup_failed.
5Company-token resolve/refreshOrganizationConnection (managed credential)Any row 2–4 call returning 401CredentialResolver supplies the managed company token; on 401 the client refreshes via the company-token SSO endpoint and persists the new token (same pattern as the CRM HTTP client), then retries once.• Refresh fails → surface auth error per rows 2–3; log ai_agent_booking_auth_refresh_failed.
[Engineering owns in the RFC: exact NodeRegistry properties JSON (destinations, depends_on
chain location→service→slot), the response_fields pruning set for find_booking, the
Idempotency-Key derivation, the org→tenant (external_company_id) mapping contract with the
appointment service, and the appointment-side company-token acceptance design (DEP-1).]

📊 API Sequence — create booking (happy path + slot conflict) (PROPOSAL — Engineering owns final design)

sequenceDiagram
participant C as Customer
participant AIS as AI Service (function-calling)
participant BE as Chatbot BE (executor)
participant APPT as Qontak Appointment API
C->>AIS: "Can I book a consultation Friday afternoon?"
AIS->>BE: qontak_booking_find / slot lookup (row 4)
BE->>APPT: GET /appointments/slots (X-Source-Id: ai, company token)
APPT-->>BE: available slots
BE-->>AIS: compact slot list
AIS-->>C: "Friday 14:00 or 15:30 — which works?"
C->>AIS: "14:00"
AIS->>BE: qontak_booking_create(args)
BE->>APPT: POST /appointments/bookings (Idempotency-Key)
alt Slot still free
APPT-->>BE: 201 booking created (staff round-robin assigned)
BE-->>AIS: booking id + confirmed slot
AIS-->>C: "Booked! Friday 14:00, booking #…"
else Slot taken meanwhile
APPT-->>BE: 409 / validation error
BE-->>AIS: structured slot_conflict error
AIS->>BE: re-check slots
AIS-->>C: "14:00 just filled — 15:30 instead?"
end

10. System Flow + User Stories + ACs

10.1 System Flow

Flow: AI Agent books and finds appointments mid-conversation
Type: User Journey + API Sequence

CONFIG (build time)
1. Builder opens AI Agent config → Actions → Add action → "Create Booking" (and/or
"Find Booking") — visible only when the flag is ON and the org has an appointment
tenant.
2. Builder pins the location (pre-selected for single-location orgs), chooses per-param
fixed-vs-AI-filled for service, writes the trigger description, saves.
3. On train/sync, the actions are registered as callable tools (qontak_function_call)
with the AI-filled params as the LLM arg schema.

RUNTIME (conversation)
4. Customer signals booking intent ("can I book…", "when is my appointment?").
5. FIND path — agent calls qontak_booking_find (phone/name/date range) → compact
booking list returned → agent answers; zero matches → agent offers to create.
6. CREATE path — agent first checks slot availability (slots lookup, part of the
action contract), offers concrete options, customer picks one.
7. Agent calls qontak_booking_create with service, date, start, customer name + phone
(collected conversationally), staff omitted → appointment round-robin assigns.
8. Appointment service validates everything server-side (business hours, closed days,
time-off, conflicts, buffers) and returns the created booking → agent confirms with
booking id, time, and assigned staff.
9. FAILURE BRANCH — slot taken between check and create (409) → agent re-checks and
offers alternatives (one retry loop, then handoff).
10. FAILURE BRANCH — auth failure after one token refresh, or timeout → agent does NOT
claim success; apologizes and falls back to the account's existing human-handoff
behavior. Idempotency-Key guarantees no double-booking on retries.

📊 System Flow — AI Agent Appointment Booking

sequenceDiagram
participant C as Customer
participant A as AI Agent (function-calling)
participant BE as Chatbot Backend
participant APPT as Qontak Appointment
C->>A: Booking intent ("book me Friday" / "when is my appointment?")
alt Find Booking
A->>BE: qontak_booking_find(phone/name/dates)
BE->>APPT: GET /appointments/bookings (ai, company token)
APPT-->>BE: matching bookings
BE-->>A: compact list (max 10)
A-->>C: booking details / "none found — want to book?"
else Create Booking
A->>BE: slots lookup for service+date
BE->>APPT: GET /appointments/slots
APPT-->>BE: available slots
A-->>C: concrete slot options
C->>A: picks a slot
A->>BE: qontak_booking_create(service, date, start, name, phone)
BE->>APPT: POST /appointments/bookings (Idempotency-Key, staff omitted)
alt Created
APPT-->>BE: 201 + booking (round-robin staff)
BE-->>A: booking id + slot + staff
A-->>C: confirmation with booking id
else Slot conflict (409)
APPT-->>BE: 409 slot taken
BE-->>A: structured slot_conflict
A-->>C: offer alternative slots (then handoff if exhausted)
end
end
Note over A,BE: Auth failure after refresh, or timeout →<br/>agent never claims success; existing handoff fires

10.2 User Stories

User StoryImportanceMockup / Technical NotesAcceptance Criteria
[APPTACT-S01] — Builder configures the booking actions

As a CS Ops Lead / Bot Manager, I want to add "Create Booking" and "Find Booking" actions to my AI Agent, pinning the location and writing trigger descriptions, so that the agent can book and look up appointments for my business autonomously.
Must HaveFigma: N/A — no reference design exists yet (Section 15 dependency); reuse the CRM-action drawer pattern

Data Fields:
name (string, required) — User input
parameters.trigger (string, required) — User input
location_id (resource lookup, required) — User selection; pre-selected when the org has exactly one active location
service_id (resource lookup) — per-param mode: fixed by builder OR use_ai: true
• per-param use_ai flags for date/start/customer fields (default AI-filled)

Before-After Behavior: Before: no booking actions exist anywhere in the action catalog — the AI Agent cannot touch the appointment service. After: builders configure the two actions in the same drawer family as CRM actions; on train they become callable tools.
— Happy Path —
• AC-1: Given the flag rollout_ai_agent_booking_action is ON (per S6 §6.1) and the org has an active appointment tenant, when the builder opens Add action, then "Create Booking" and "Find Booking" appear in the Mekari Qontak group.
• AC-2: Given the Create Booking drawer, when the builder selects a location, writes a trigger description, and saves, then the action is persisted and appears in the agent's action list; on next train it is registered as a callable tool.
• AC-3: Given the org has exactly one active location, when the drawer opens, then that location is pre-selected.

— Error / Unhappy Path —
• ERR-1: Given the org's appointment tenant has zero active services, when the builder opens the Create Booking drawer, then Save is blocked with a prompt to publish a service in Appointment first.
• ERR-2: Given the appointment lookups fail or time out, when the drawer is open, then an error state with Retry is shown and Save stays blocked (per S8 UI states).
• ERR-3: Given a multi-location org, when no location is selected, then Save is blocked with a required-field error.

— Permission Model —
• CAN: Admin, Bot Manager (roles with AI Agent config edit rights)
• CANNOT: Agent/viewer roles without AI Agent edit rights
• Unauthorized: "Add action" not rendered (existing config-permission behavior).

— UI States —
• Loading: location/service lookups fetching
• Empty: tenant has no active services
• Error: lookup failure + Retry
• Success: required fields valid → Save enabled
[APPTACT-S02] — Agent creates a booking mid-conversation

As an AI Agent (on behalf of the builder), I want to check real slot availability, offer options, and create the booking the customer picks, so that the conversation ends in a confirmed appointment without a human touch.
Must HaveFigma: N/A (runtime, no builder UI)

Data Fields:
service_id (int, required) — fixed or AI-filled per config
date (string YYYY-MM-DD, required) + start (string HH:MM, required) — AI-filled after slot check
customer_name (string, required), customer_phone (E.164, required) — AI-filled from conversation
staff_id — OMITTED in v1 (appointment round-robin assigns; per S5 Non-Goal 5)
Idempotency-Key (header, derived from room + invocation; per S6 §6.5)

Before-After Behavior: Before: the agent can only discuss availability in free text and must hand off for the actual booking. After: the agent verifies real slots, creates the booking via the appointment API, and confirms with the booking id — validation (hours, closed days, time-off, conflicts, buffers) stays fully server-side in the appointment service.
— Happy Path —
• AC-1: Given an enabled Create Booking action and a customer who has picked an offered slot, when the agent calls qontak_booking_create, then a booking is created via POST /appointments/bookings with X-Source-Id: ai + company token + Idempotency-Key, and the agent's confirmation includes the booking id, date/time, and assigned staff name.
• AC-2: Given the customer states no staff preference, when the booking is created, then staff_id is omitted and the appointment service's round-robin assigns the staff — the agent does not pick.
• AC-3: Given the agent has not yet checked availability for the requested date, when it prepares to create, then it first calls the slots lookup and only offers times that are actually available (tool contract per S9 row 4).

— Error / Unhappy Path —
• ERR-1: Given the picked slot was taken between check and create, when the API returns the conflict error, then the agent re-checks slots and offers alternatives (max 1 automatic retry loop), and ai_agent_booking_create_failed is logged with reason slot_conflict.
• ERR-2: Given the executor call times out (per S6 §6.6), when the agent's turn continues, then the agent does NOT claim the booking succeeded, offers human handoff, and the same Idempotency-Key on any retry returns the existing booking instead of duplicating it.
• ERR-3: Given the company token is rejected and one refresh also fails, when the action errors, then ai_agent_booking_auth_refresh_failed is logged and the agent falls back to the account's existing handoff behavior.
• ERR-4: Given the customer's phone cannot be captured in E.164 form, when the agent attempts to create, then the API's validation error is surfaced to the agent, which asks the customer to re-confirm the number (no client-side silent fixup).

— Permission Model —
• CAN: the AI Agent runtime only (function-calling)
• CANNOT: the agent cannot reassign/relocate/resolve-conflicts or set completed/no-show — server-blocked by the appointment aiAllowedActions allowlist (per S6 §6.7), regardless of prompt injection
• Unauthorized: tool not registered when the action is disabled or the flag is OFF.

— UI States —
• Loading: N/A (synchronous tool call in the agent loop)
• Empty: no available slots for the requested date → agent offers other dates
• Error: per ERR-1..4; conversation never blocked
• Success: confirmation message with booking id in the conversation
[APPTACT-S03] — Agent finds the customer's bookings

As an AI Agent (on behalf of the builder), I want to look up a customer's existing bookings by phone, name, or date range, so that I can answer "when is my appointment?" and avoid duplicate bookings without a human checking the calendar.
Must HaveFigma: N/A (runtime, no builder UI)

Data Fields:
customer_phone (E.164) OR q (name query) — AI-filled, at least one required
from_date / to_date (optional), status (optional enum)
• Response pruned via response_fields: id, service, date, start, status, staff — max 10, newest first (per S6 §6.6)

Before-After Behavior: Before: "when is my appointment?" always requires human lookup in the appointment dashboard. After: the agent answers directly from a compact read-only query; before creating, it can check for existing bookings to avoid duplicates.
— Happy Path —
• AC-1: Given a customer asks about their appointment, when the agent calls qontak_booking_find with the conversation's phone number, then matching bookings return (compact fields, max 10, newest first) and the agent answers with date, time, service, and staff.
• AC-2: Given zero bookings match, when the result returns, then the agent says none was found and offers to create one (bridging to S02).
• AC-3: Given the customer asks to book but already has a booked appointment for the same service in the same date range, when the agent has just run find, then it surfaces the existing booking and asks before creating a second one.

— Error / Unhappy Path —
• ERR-1: Given the appointment API errors or times out, when the find call fails, then ai_agent_booking_lookup_failed is logged, and the agent offers handoff instead of guessing.
• ERR-2: Given neither a phone nor a name query can be established from the conversation, when the agent considers calling find, then it first asks the customer for the identifying detail (tool arg schema marks one-of as required).

— Permission Model —
• CAN: the AI Agent runtime only; read-only
• CANNOT: no mutation of found bookings (reschedule/cancel are Non-Goals 1); no cross-tenant reads — tenant scoping enforced server-side by the appointment service
• Unauthorized: tool not registered when disabled/flag OFF.

— UI States —
• Loading: N/A
• Empty: zero matches → offer to create
• Error: per ERR-1
• Success: booking details in the agent's reply
[APPTACT-S04] — Builder enables/disables and rollout gating

As a CS Ops Lead / Bot Manager, I want to turn the booking actions on or off per agent (subject to the account flag), so that I control exactly when my agent starts and stops booking.
Should HaveFigma: N/A — reuse existing action enable/disable + agent train flow

Data Fields:
enabled (bool) — User input
rollout_ai_agent_booking_action (flag, default OFF) — Account config (per S6 §6.1)

Before-After Behavior: Before: no booking actions exist to toggle. After: per-agent enable/disable with account-level flag gating during rollout — same lifecycle as every other catalog action.
— Happy Path —
• AC-1: Given the flag is ON, when the builder enables the action and trains, then the tool is callable; when disabled and re-trained, then the agent can no longer call it.

— Error / Unhappy Path —
• ERR-1: Given the flag is OFF for the account, when the builder opens the action list, then neither booking action is rendered (per S6 §6.1).
• ERR-2: Given an action was disabled after a conversation started, when the agent would otherwise call it, then the tool is absent from the synced skill pack after re-train; pre-existing in-flight turns fail gracefully to handoff.

— Permission Model —
• CAN: Admin, Bot Manager
• CANNOT: roles without AI Agent edit rights
• Unauthorized: toggle not rendered.

— UI States —
• Loading: save/train in progress
• Empty: N/A
• Error: save failure toast
• Success: enabled/disabled state visible

Dependencies: S02 and S03 depend on S01 (actions must exist and be configured) and on DEP-1 (appointment company-token acceptance — Section 15). S04 gates S01–S03 via the feature flag.


NEGATIVE SCENARIOS (from Section 5 Non-Goals)

NEG-1: Given a customer asks the agent to move or cancel an existing booking,
When the agent has only create/find tools,
Then no reschedule/cancel occurs — the agent offers human handoff. [Non-Goal 1]

NEG-2: Given any prompt (including injection) asking the agent to reassign staff,
relocate a booking, or resolve conflicts,
When such a call reaches the appointment service with X-Source-Id: ai,
Then the service rejects it server-side (403) — the allowlist is not widened. [Non-Goal 2]

NEG-3: Given a conversation about attendance,
When the agent is asked to mark a booking completed or no-show,
Then no status change occurs — server-blocked for AI callers. [Non-Goal 3]

NEG-4: Given the appointment product's booking rules (hours, buffers, conflicts),
When the agent books,
Then no rule is bypassed or re-implemented chatbot-side — validation stays in the
appointment service. [Non-Goal 4]

NEG-5: Given a customer with no staff preference,
When the booking is created,
Then the agent never picks a staff member — staff_id is omitted and round-robin
assigns. [Non-Goal 5]

NEG-6: Given a booking is created,
When the turn completes,
Then no payment/deposit is requested by the agent. [Non-Goal 6]

NEG-7: Given any booking action succeeds,
When the chatbot backend processes the result,
Then no booking data is persisted chatbot-side beyond the standard action event
log. [Non-Goal 7]

🧪 Test Coverage Matrix — [APPTACT-S01]

DimensionCoverageNotes
Boundary values⚠️ partialERR-1 covers zero services; AC-3 covers single-location. ⚠️ QA: org with a very large number of services/locations — picker performance
State transitions✅ definedS8 state diagram: Loading→Ready/Empty/Error→Saved
Data validation⚠️ TBD⚠️ QA: special characters in action name/trigger; location id belonging to another org's tenant
Concurrency⚠️ TBD⚠️ QA: two builders editing the same agent's actions simultaneously
Network/timeout✅ definedERR-2 covers lookup timeout with Retry

🧪 Test Coverage Matrix — [APPTACT-S02]

DimensionCoverageNotes
Boundary values⚠️ partialEmpty-state covers zero slots. ⚠️ QA: booking at the exact business-hours boundary; last available slot of the day; date at the tenant timezone's day boundary
State transitions✅ definedcheck→offer→create; ERR-1 conflict loop capped at 1 retry
Data validation✅ definedERR-4 covers non-E.164 phone; server-side validation is the contract
Concurrency✅ definedERR-1 (slot raced away) + §6.5 idempotency (double-send of same invocation)
Network/timeout✅ definedERR-2 timeout + idempotent retry

🧪 Test Coverage Matrix — [APPTACT-S03]

DimensionCoverageNotes
Boundary values✅ definedAC-2 zero matches; cap at 10 results defined in §6.6
State transitions✅ definedfind→answer / find→create bridge (AC-3)
Data validation⚠️ TBD⚠️ QA: name query with special characters/emoji; phone formats customers actually type
Concurrency⚠️ TBD⚠️ QA: find while a create for the same customer is in flight in another room
Network/timeout✅ definedERR-1

11. Rollout

StageAudienceDurationExit criteria
1 — Internal QA1 dogfood org on staging, wired to a staging qontak-appointment tenant1–2 weeksAll Must Have ACs pass; zero double-bookings under the concurrency QA items; DEP-1 auth path verified end-to-end
2 — Closed Beta3–5 appointment-led orgs (EAP candidates of the Cerdas Lab profile — education/clinic/services), flag ON per account2–4 weeks≥ 20 AI-created bookings across beta orgs; create failure rate (excl. legitimate slot conflicts) < 2%; no severity-1 booking-integrity incident
3 — GAFlag default flips ON for eligible orgs (AI Agent + appointment tenant); announcement + docsPost-launch monitoring cadence green for 4 weeks (per §12.6)

No data migration is involved — the feature is net-new configuration; existing agents and existing bookings are untouched (no transition-window behavior to define).

Rollback: flip rollout_ai_agent_booking_action OFF per account (or globally) → actions vanish from the picker and, after re-train/sync, from the agents' tool sets. Bookings already created remain valid records in the appointment service — rollback stops new AI bookings, it does not touch existing ones.

12. Observability

#EventTriggerKey properties
1ai_agent_booking_create_invokedAgent calls qontak_booking_createorg_id, agent_id, room_id, location_id, service_id
2ai_agent_booking_create_succeeded201 from the appointment APIbooking_id, round_robin_assigned (bool), latency_ms
3ai_agent_booking_create_failedAny create failurereason enum: slot_conflict | validation | auth_failed | timeout, latency_ms
4ai_agent_booking_find_invoked / _failedAgent calls qontak_booking_findmatch_count, latency_ms; failure reason enum
5ai_agent_booking_lookup_failedSlot/catalog lookup failure at runtimeendpoint, reason
6ai_agent_booking_auth_refresh_failedCompany-token refresh failsorg_id

Dashboard owner: Hadiningbot Squad (same Datadog/event-log surface as the existing action family; rows also flow into the per-response ai_activity_logs datamart via the standard action references). Alert thresholds: create failure rate (excl. slot_conflict) > 5% over 1h → page; auth_refresh_failed > 10/h for one org → investigate that org's connection.

12.6 Post-Launch Monitoring Cadence

Weekly review for the first 4 weeks post-GA (owner: PM + squad lead). Investigation triggers: create failure rate (excl. slot conflicts) > 2% week-over-week; slot-conflict rate > 15% of creates (signals the agent is offering stale slots); any double-booking report (severity-1 — freeze via flag and investigate idempotency + appointment-side race).

13. Success Metrics

CategoryMetricBaselineTarget
AdoptionAI booking containment — % of AI-Agent conversations with booking intent that end in an AI-created booking (no human touch)0% (capability does not exist)≥ 40% across beta orgs within 30 days of their flag-ON date
AdoptionOrgs with ≥ 1 booking action enabled (of eligible orgs: AI Agent + appointment tenant)0≥ 50% of eligible orgs within 60 days of GA
QualityCreate failure rate, excluding legitimate slot conflictsN/A< 2%
QualityDouble-bookings attributable to AI actionsN/A0 (hard invariant — idempotency + server validation)
EfficiencyMedian time from booking intent to confirmed booking in-conversationN/A (today: human callback, hours)< 3 minutes

Instrumentation: events in Section 12 + rooms.closed_reason containment spine (RESOLVE_AI) joined with ai_agent_booking_create_succeeded per room.

14. Launch Plan & Stage Gates

GateCriteria to passOwner
G1 — Build → Internal QADEP-1 (appointment company-token auth) merged and verified on staging; both executors + NodeRegistry rows live on staging; FE picker behind flagEng lead
G2 — Internal QA → Closed BetaAll Must Have ACs green; concurrency/timeout QA items from the Test Coverage Matrices executed; observability events visible on the dashboardQA (Lane B — human gate, per initiative README)
G3 — Closed Beta → GABeta exit criteria met (§11 stage 2); no open severity-1; support/docs readyPM

15. Dependencies

#DependencyOwnerWhat's neededBlocking?
DEP-1qontak-appointment service-to-service authAppointment squad (same-repo change; no external team)Accept the company token and resolve org → tenant via external_company_id; existing user-session auth unchanged (CHG-004). Today the auth middleware only accepts user-session Bearers — without this, the executor cannot call the API at all.YES
DEP-2NodeRegistry + AiAgentTool seeding (RailsAdmin)Chatbot BE + opsTwo NodeRegistry rows (schemas incl. destination/use_ai/is_rl/depends_on) and AiAgentTool rows; a repeatable seeding note so staging/production don't driftYES
DEP-3Design — config drawerDesign (Wulan)Figma for the booking-action drawer (location/service pickers). Mitigation: the drawer reuses the existing CRM-action pattern; build can start from that pattern if design lagsNO
DEP-4Feature flag + FE visibility plumbingChatbot FErollout_ai_agent_booking_action system preference + isBookingNode gate + endpoint-flag-map rowsNO (standard, within squad)

Explicitly NOT dependencies: DSAI/AI-Service — the sync layer (sync_to_ai_service.rb) and the execute callback (action_execute.rb) are generic and require zero AI-Service-side changes (dependency-relevancy check done: nothing crosses to DSAI). Billing — booking actions ride the existing action/deduction pipeline unchanged.

📊 Dependency Graph (PROPOSAL — Engineering to validate blocking status in RFC)

graph LR
F[Appointment Booking actions] -->|BLOCKING| D1[DEP-1: appointment company-token auth]
F -->|BLOCKING| D2[DEP-2: NodeRegistry + AiAgentTool rows]
F -->|non-blocking| D3[DEP-3: Figma drawer design]
F -->|non-blocking| D4[DEP-4: FE flag + visibility gate]

16. Key Decisions + Alternatives Rejected

Decisions Made

#DecisionRationale
D1New executor group mekari_qontak_booking (not an extension of mekari_qontak_crm)Different target host, different credential row, different failure semantics (slot conflicts); keeps ACTION_TYPE_MAPPINGs independent and blast radius small
D2Auth = company token (qontak-action bucket), accepted by the appointment service (DEP-1, Option A)Matches the initiative's identity (first-party, company token); a single auth model on the Go service; org→tenant mapping falls out of external_company_id for free
D3staff_id omitted in v1 → appointment round-robin assignsFairness logic already exists server-side and is load-balanced; agent-side staff choice adds prompt-injection and favoritism surface for no v1 value
D4v1 = create + find only (reschedule/cancel deferred)Smallest catalog slice that completes the core containment loop (book + answer "when is my appointment?"); both deferred actions are already AI-whitelisted server-side so nothing is architecturally foreclosed
D5Slot check is part of the Create Booking tool contract (agent must check before offering)Prevents hallucinated availability; turns 409s into a rare race case rather than the normal path
D6find_booking responses pruned to compact fields, max 10Agent-context economy; mirrors the CRM _read actions' response_fields pattern

Alternatives Rejected

#AlternativeWhy rejected
A1Extend MekariQontakCrm::Execute with booking action typesCouples booking failures/auth to the CRM client; different base URL and error taxonomy; violates the one-group-per-target pattern the registry already follows
A2HMAC service-to-service auth (mekari-action pattern) into the appointment APIWrong taxonomy bucket for a Qontak-owned target; adds a second auth model to the Go service; requires SSO Super Admin approval flow the first-party bucket exists to avoid
A3Have the AI Service (DSAI) call the appointment API directly as a native toolCreates a DSAI dependency (slow/unreliable per standing rule) where a squad-owned path exists; bypasses the action framework's credential, logging, and billing plumbing
A4Use a WhatsApp Flow form to capture booking fields, then bookDepends on the Send WhatsApp Flow action (itself DRAFT); conversational capture ships now; the combination is deliberately deferred (S5 Non-Goal 8)
A5Agent-side availability cache to avoid the pre-create slot checkStaleness creates double-offer risk; the appointment slots endpoint is cheap and already AI-whitelisted

17. Open Questions

#TypeQuestionOwnerDeadline
1Open QuestionDEP-1 design: does the appointment service accept the company token directly (extend auth.Client), or via a short-lived exchanged service token? Also confirm the org→tenant mapping contract (external_company_id) covers every beta orgEng (appointment + chatbot BE), RFCRFC review
2Open QuestionMulti-location orgs: v1 pins one location per action instance (D1 pattern — configure one action per location?). Confirm this is acceptable for beta orgs or whether location must be AI-selectablePM + beta org feedbackEnd of Closed Beta
3RiskThe appointment service has a known TOCTOU double-booking race under concurrent creates (no transaction/lock around validate+insert). AI traffic raises concurrency. Mitigation: idempotency key kills same-invocation dupes; ERR-1 handles 409s; flag as input to the appointment squad's backlog — a server-side fix (unique constraint / locking) is recommended before GA scaleAppointment squadBefore G3 (GA)
4Open QuestionRate limiting: does the appointment API need a per-tenant rate limit for AI callers before GA (runaway-agent protection)?Eng, RFCRFC review
5AssumptionThe existing action billing/deduction pipeline applies to booking actions unchanged (no new pricing decision for beta)PMBefore G2

PRD CHANGELOG

VersionDateAuthorSectionChangeSummary
1.12026-07-25Dimas Fauzi Hidayat (w/ Claude)Section 8 (New Features)ADDEDWireframes for the three screens (picker entry, Create Booking drawer, Find Booking drawer) embedded from ../wireframes/*.svg, grounded in the qontak-designer action-drawer prototype (ActionConfigBase / ActionFieldRow / Get-deal-info patterns); design gaps for the Figma pass listed under DEP-3
1.02026-07-25Dimas Fauzi Hidayat (w/ Claude)AllCREATEDInitial PHASE PRD for the Appointment Booking catalog item (create_booking & find_booking), grounded in the chatbot action framework (executor registry, NodeRegistry, SyncToAiService) and the qontak-appointment API (AI allowlist, idempotency, round-robin) mapped 2026-07-25; PIH source: agentic-ai-early-access interview 2026-02-24 (Cerdas Lab lead-to-appointment workflow)