Qontak | Chatbot | AI Agent — Qontak Action — Appointment Booking Actions
HEADER BLOCK
| Field | Value |
|---|---|
| PM | Dimas Fauzi Hidayat |
| PRD Version | 1.1 |
| Status | DRAFT |
| PRD Type | PHASE |
| Epic | TBD — create once RFC is agreed (child Epic per the initiative's umbrella model) |
| Squad | Hadiningbot Squad |
| Product Module | Bot, AI, and Automation |
| RFC Link | N/A — pending |
| Figma Master | N/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. |
| Anchor | Qontak Action — ANCHOR |
| Labels | epic:qontak-chatbot | module:ai-agent | feature:appointment-booking-actions |
| Last Updated | 2026-07-25 |
Status values:
DRAFT→READY→BUILD→SHIPPEDREADY gate: Epic cannot move to In Progress in Jira without PRD Link + RFC Link populated.
Table of Contents
- HEADER BLOCK
- 2. CONDITIONAL BLOCK: PHASE CONTEXT
- 3. One-liner + Problem
- 4. Target Users + Persona Context
- 5. Non-Goals
- Scope Changes
- 6. Constraints
- 7. Feature Changes
- 8. New Features
- 9. API & Webhook Behavior
- 10. System Flow + User Stories + ACs
- 11. Rollout
- 12. Observability
- 13. Success Metrics
- 14. Launch Plan & Stage Gates
- 15. Dependencies
- 16. Key Decisions + Alternatives Rejected
- 17. Open Questions
- PRD CHANGELOG
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
| Persona | Role | Goal | Pain | Workaround |
|---|---|---|---|---|
| Primary — CS Ops Lead / Bot Manager | Owns the AI Agent configuration for an appointment-led business | The agent books qualified customers into real slots end-to-end | Agent stops at "our admin will contact you" — booking requires a human on the appointment dashboard | Human 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 Agent | Pick a service and time and get a confirmed booking in the same conversation | Redirected to a link or told to wait for a callback; drop-off before booking | Calls the business or books elsewhere |
| Secondary — Human agent / admin | Handles handoffs and manages the appointment calendar | Only handle bookings that genuinely need judgment (conflicts, special requests) | Every AI conversation that reaches "book me" lands in their queue as manual data re-entry | Copy-pastes customer name/phone/slot from chat into the Appointment app |
5. Non-Goals
- 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.
- No AI access to reassign / relocate / resolve-conflicts. The appointment service blocks these for
X-Source-Id: aicallers by design; this PRD does not ask to widen the allowlist. - No AI-set
completed/no-showstatuses. Server-blocked for AI callers; attendance truth stays human-owned. - 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.
- No new staff-selection logic. When the customer has no staff preference the action omits
staff_idand the appointment service's existing round-robin assigns fairly; this PRD does not build agent-side staff ranking. - No payment/deposit collection at booking time.
- No booking-data storage in the chatbot backend. Bookings live solely in the appointment service; the chatbot keeps only its standard action event logs.
- 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.
- Backend —
chatbot: newmekari_qontak_bookingexecutor group (node_type_registry.rbGROUP entry,nodes/mekari_qontak_booking/execute.rbwithACTION_TYPE_MAPPINGforqontak_booking_create/qontak_booking_find), newbooking_http_client.rb(company-token auth viaCredentialResolver, sendsX-Source-Id: ai+Idempotency-Key), newnode_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). - Frontend —
chatbot-fe:ACTION_LISTentries for the two booking node types (ai-agent-actions-constants.ts),isBookingNodepredicate + 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-designerprototype exists yet (TBD — Section 15).
6. Constraints
| # | Constraint | Detail |
|---|---|---|
| 6.1 | Feature flag | rollout_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.2 | Plan / eligibility | Org 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.3 | Platform | Config: web only (AI Agent config UI). Runtime: channel-agnostic — the actions run wherever the autonomous agent runs; primary launch channel is WhatsApp. |
| 6.4 | Auth | Company 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.5 | Idempotency | Every 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.6 | Performance | Action 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.7 | Server-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.8 | Data lifecycle | No 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 ID | Change Type | Surface | Before | After |
|---|---|---|---|---|
| CHG-001 | ADD | chatbot BE — node_type_registry.rb GROUP | 5 executor groups (api, mekari_qontak_crm, mekari_qontak_chat, google_sheets, google_calendar) | +mekari_qontak_booking → MekariQontakBooking::Execute, with ACTION_TYPE_MAPPING: qontak_booking_create → POST /appointments/bookings, qontak_booking_find → GET /appointments/bookings |
| CHG-002 | ADD | chatbot BE — NodeRegistry + AiAgentTool rows | No booking node types exist | Two 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-003 | ADD | chatbot-fe — action picker | ACTION_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-004 | MODIFY | qontak-appointment BE — auth middleware | Only user-session Bearer tokens (Qontak SSO) accepted; no service-to-service path | Company 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:
Create Booking config drawer — every configurable field (APPTACT-S01 data fields):
Find Booking config drawer — search params are runtime/AI-filled; the builder only curates what information returns:
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
| # | Behavior | Entity Affected | Triggered By | Expected Behavior | Failure Behavior |
|---|---|---|---|---|---|
| 1 | Save booking action config | AiAgentAction / agent parameters.capabilities[].actions[], AiAgentTool | Builder clicks Save in the config drawer | Persist 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. |
| 2 | Agent invokes Create Booking at runtime | A 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 customer | Executor 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. |
| 3 | Agent invokes Find Booking at runtime | None (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. |
| 4 | Slot / catalog resource lookups | None (read-only) | (a) Builder opens the config drawer — services/staff/locations lists; (b) Agent checks availability mid-conversation before creating | lookup_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. |
| 5 | Company-token resolve/refresh | OrganizationConnection (managed credential) | Any row 2–4 call returning 401 | CredentialResolver 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 Story | Importance | Mockup / Technical Notes | Acceptance 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 Have | Figma: 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 Have | Figma: 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 Have | Figma: 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 Have | Figma: 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]
| Dimension | Coverage | Notes |
|---|---|---|
| Boundary values | ⚠️ partial | ERR-1 covers zero services; AC-3 covers single-location. ⚠️ QA: org with a very large number of services/locations — picker performance |
| State transitions | ✅ defined | S8 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 | ✅ defined | ERR-2 covers lookup timeout with Retry |
🧪 Test Coverage Matrix — [APPTACT-S02]
| Dimension | Coverage | Notes |
|---|---|---|
| Boundary values | ⚠️ partial | Empty-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 | ✅ defined | check→offer→create; ERR-1 conflict loop capped at 1 retry |
| Data validation | ✅ defined | ERR-4 covers non-E.164 phone; server-side validation is the contract |
| Concurrency | ✅ defined | ERR-1 (slot raced away) + §6.5 idempotency (double-send of same invocation) |
| Network/timeout | ✅ defined | ERR-2 timeout + idempotent retry |
🧪 Test Coverage Matrix — [APPTACT-S03]
| Dimension | Coverage | Notes |
|---|---|---|
| Boundary values | ✅ defined | AC-2 zero matches; cap at 10 results defined in §6.6 |
| State transitions | ✅ defined | find→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 | ✅ defined | ERR-1 |
11. Rollout
| Stage | Audience | Duration | Exit criteria |
|---|---|---|---|
| 1 — Internal QA | 1 dogfood org on staging, wired to a staging qontak-appointment tenant | 1–2 weeks | All Must Have ACs pass; zero double-bookings under the concurrency QA items; DEP-1 auth path verified end-to-end |
| 2 — Closed Beta | 3–5 appointment-led orgs (EAP candidates of the Cerdas Lab profile — education/clinic/services), flag ON per account | 2–4 weeks | ≥ 20 AI-created bookings across beta orgs; create failure rate (excl. legitimate slot conflicts) < 2%; no severity-1 booking-integrity incident |
| 3 — GA | Flag default flips ON for eligible orgs (AI Agent + appointment tenant); announcement + docs | — | Post-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
| # | Event | Trigger | Key properties |
|---|---|---|---|
| 1 | ai_agent_booking_create_invoked | Agent calls qontak_booking_create | org_id, agent_id, room_id, location_id, service_id |
| 2 | ai_agent_booking_create_succeeded | 201 from the appointment API | booking_id, round_robin_assigned (bool), latency_ms |
| 3 | ai_agent_booking_create_failed | Any create failure | reason enum: slot_conflict | validation | auth_failed | timeout, latency_ms |
| 4 | ai_agent_booking_find_invoked / _failed | Agent calls qontak_booking_find | match_count, latency_ms; failure reason enum |
| 5 | ai_agent_booking_lookup_failed | Slot/catalog lookup failure at runtime | endpoint, reason |
| 6 | ai_agent_booking_auth_refresh_failed | Company-token refresh fails | org_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
| Category | Metric | Baseline | Target |
|---|---|---|---|
| Adoption | ⭐ AI 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 |
| Adoption | Orgs with ≥ 1 booking action enabled (of eligible orgs: AI Agent + appointment tenant) | 0 | ≥ 50% of eligible orgs within 60 days of GA |
| Quality | Create failure rate, excluding legitimate slot conflicts | N/A | < 2% |
| Quality | Double-bookings attributable to AI actions | N/A | 0 (hard invariant — idempotency + server validation) |
| Efficiency | Median time from booking intent to confirmed booking in-conversation | N/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
| Gate | Criteria to pass | Owner |
|---|---|---|
| G1 — Build → Internal QA | DEP-1 (appointment company-token auth) merged and verified on staging; both executors + NodeRegistry rows live on staging; FE picker behind flag | Eng lead |
| G2 — Internal QA → Closed Beta | All Must Have ACs green; concurrency/timeout QA items from the Test Coverage Matrices executed; observability events visible on the dashboard | QA (Lane B — human gate, per initiative README) |
| G3 — Closed Beta → GA | Beta exit criteria met (§11 stage 2); no open severity-1; support/docs ready | PM |
15. Dependencies
| # | Dependency | Owner | What's needed | Blocking? |
|---|---|---|---|---|
| DEP-1 | qontak-appointment service-to-service auth | Appointment 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-2 | NodeRegistry + AiAgentTool seeding (RailsAdmin) | Chatbot BE + ops | Two NodeRegistry rows (schemas incl. destination/use_ai/is_rl/depends_on) and AiAgentTool rows; a repeatable seeding note so staging/production don't drift | YES |
| DEP-3 | Design — config drawer | Design (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 lags | NO |
| DEP-4 | Feature flag + FE visibility plumbing | Chatbot FE | rollout_ai_agent_booking_action system preference + isBookingNode gate + endpoint-flag-map rows | NO (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
| # | Decision | Rationale |
|---|---|---|
| D1 | New 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 |
| D2 | Auth = 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 |
| D3 | staff_id omitted in v1 → appointment round-robin assigns | Fairness logic already exists server-side and is load-balanced; agent-side staff choice adds prompt-injection and favoritism surface for no v1 value |
| D4 | v1 = 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 |
| D5 | Slot 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 |
| D6 | find_booking responses pruned to compact fields, max 10 | Agent-context economy; mirrors the CRM _read actions' response_fields pattern |
Alternatives Rejected
| # | Alternative | Why rejected |
|---|---|---|
| A1 | Extend MekariQontakCrm::Execute with booking action types | Couples booking failures/auth to the CRM client; different base URL and error taxonomy; violates the one-group-per-target pattern the registry already follows |
| A2 | HMAC service-to-service auth (mekari-action pattern) into the appointment API | Wrong 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 |
| A3 | Have the AI Service (DSAI) call the appointment API directly as a native tool | Creates a DSAI dependency (slow/unreliable per standing rule) where a squad-owned path exists; bypasses the action framework's credential, logging, and billing plumbing |
| A4 | Use a WhatsApp Flow form to capture booking fields, then book | Depends on the Send WhatsApp Flow action (itself DRAFT); conversational capture ships now; the combination is deliberately deferred (S5 Non-Goal 8) |
| A5 | Agent-side availability cache to avoid the pre-create slot check | Staleness creates double-offer risk; the appointment slots endpoint is cheap and already AI-whitelisted |
17. Open Questions
| # | Type | Question | Owner | Deadline |
|---|---|---|---|---|
| 1 | Open Question | DEP-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 org | Eng (appointment + chatbot BE), RFC | RFC review |
| 2 | Open Question | Multi-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-selectable | PM + beta org feedback | End of Closed Beta |
| 3 | Risk | The 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 scale | Appointment squad | Before G3 (GA) |
| 4 | Open Question | Rate limiting: does the appointment API need a per-tenant rate limit for AI callers before GA (runaway-agent protection)? | Eng, RFC | RFC review |
| 5 | Assumption | The existing action billing/deduction pipeline applies to booking actions unchanged (no new pricing decision for beta) | PM | Before G2 |
PRD CHANGELOG
| Version | Date | Author | Section | Change | Summary |
|---|---|---|---|---|---|
| 1.1 | 2026-07-25 | Dimas Fauzi Hidayat (w/ Claude) | Section 8 (New Features) | ADDED | Wireframes 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.0 | 2026-07-25 | Dimas Fauzi Hidayat (w/ Claude) | All | CREATED | Initial 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) |