Skip to main content

Qontak | Chatbot | AI Agent — Native Integrations — Phase 1: Google Calendar — In-Conversation Booking

Google Calendar provider — Phase 1 PRD under the Native Integration ANCHOR. Google Calendar is an OAuth provider of the Native Integration umbrella. Imported from Confluence on 2026-06-17. Code status: Google OAuth2 + Calendar resource provider are shipped (BOT-4278, BOT-4292); the in-conversation booking action + config drawer are not yet in production chatbot-fe.

NEW PRD — Phase 1 of Native Integrations. Parent ANCHOR: Qontak | Chatbot | AI Agent — Native Integrations — ANCHOR · AI-Readiness: 9.7+ / 10.0 — READY (DRAFT pending PM final review).

First initiative under new AI SDLC workflow — UI generated by AI coding agent via MCP Pixel, no Figma design.


🎨 Designer Review Notes — for Wulan & Rizky

Review owner: Wulan Febyazzahra + Rizky Surur (UI Reviewers + MCP Pixel Prompt Curators)
Review gate: Internal Alpha (S6c) — MCP Pixel output must pass sign-off before Closed Beta
Reference companion page: Wireframes + MCP Pixel Prompts — every screen + Mekari Pixel prompt block

Sections you need to review

SectionWhat to reviewWhy
S11 — New Features (UI) 🎨UI Generation Approach (MCP Pixel pattern), Action drawer IA + 4 canonical drawer states, Screen 3 (per-action details form, Pixel MCP-only spec), N2 inline OAuth status row state machine, brand-asset guardThis is the design source of truth — MCP Pixel reads from S11 prose
GCAL-S01 🎨 — Connect AI Agent to Google Calendar via OAuthUI Reference, all UI States (Empty / Loading / Error / Success), Permission ModelFrontend story — admin-facing OAuth flow surfaced via N2 inline OAuth status row inside Screen 3 (Action details form)
GCAL-S02 🎨 — Configure a Calendar templateUI Reference for the Action details form (Screen 3, extends E7) — Calendar dropdown field + all template fields with MpFormControl validation, empty/loading/error/success states for the form, attached-action row in the populated Actions tab (Screen 1, extends E3)Frontend story — largest form surface in P1; one booking template = one attached action row, no separate template list surface
GCAL-S05 🎨 — Handle token refresh failure / re-authorizeUI Reference, N2 inline OAuth status row state transitions (connected → connection-lost → re-auth → connected) and the Re-authorize UX inside Screen 3 (Action details form)Frontend story — error recovery UX
GCAL-S06 🎨 — Cascade delete AI Agent + tokens + templatesUI Reference — parent AI Agent delete modal (Screen 5, owned by parent AI Agent PRD) extended with cascadeWarnings prop for Calendar-specific copyFrontend story — extends existing parent AI Agent delete modal

Stories you do NOT need to review (backend-only — no UI)

StoryWhy no designer review
GCAL-S03 — Agent looks up available slotsBackend behavior; status reflected via N2 inline OAuth status row inside Screen 3 (reviewed in S05)
GCAL-S04 — Agent creates calendar event + sends inviteBackend behavior; success/failure reflected via N2 inline OAuth status row
GCAL-S07 — Auto-refresh OAuth access tokenBackend behavior; failure cascades to N2 inline OAuth status row inside Screen 3 (reviewed in S05)

What "review" means in practice (Internal Alpha gate)

  1. Run MCP Pixel against each prompt in the Wireframes page → get generated Vue 3 code
  2. Validate output uses correct Mp* components, v2.4 Default tokens, dark mode renders
  3. If MCP Pixel output drifts from Mekari Pixel patterns → refine the S11 prompt content + regenerate
  4. Sign off in S6c Internal Alpha gate before Closed Beta opens
  5. If quality fails: fall back to manual UI build by Hadiningbot in Phase 1.5 (per S9 Risk #6)

FieldValue
PMDimas Fauzi Hidayat
UI Reviewers + MCP Pixel Prompt CuratorsWulan Febyazzahra, Rizky Surur
Product OpsDevina Amalia
Tech LeadEko Aprianto
PRD Version1.11
StatusDRAFT
PRD TypeNEW
EpicTBC (S9 #5)
SquadHadiningbot
RFC LinkRFC — Google Sheets Workflow Nodes (Eko Aprianto, 2026-05-19, DRAFT) — canonical BE source of truth for credential storage + OAuth refresh + HTTP client + node_registries + lookup resources + feature flag patterns. Google Calendar MUST mirror these patterns.
Figma MasterN/A — UI generated via MCP Pixel by AI coding agent. See S11 §UI Generation Approach.
AnchorYes — parent ANCHOR
Labelsepic:qontak-chatbot | module:ai-agent | feature:native-integrations
Last Updated2026-05-22

S1 — One-liner + Problem

One-liner: Enable Qontak chatbot builders to grant their AI Agent direct access to Google Calendar, so the agent can book appointments end-to-end inside customer conversations.

Problem: Today, when a Qontak AI Agent collects a high-intent lead inside a chat — a customer asking to book a test drive, schedule a property viewing, request a consultation, set up a demo, or reserve a service slot — the agent cannot complete the booking inside the conversation. The agent has to hand off to a human who manually opens Google Calendar, finds an open slot, creates the event, and emails the customer back; this handoff loses ~40-60% of hot leads to drop-off (Assumption — needs validation in S9). Without a native Google Calendar action, AI Agent's value caps at "qualify the lead" instead of "close the booking" — and Qontak cannot demonstrate the AI Agent as a true conversion engine across its cross-vertical customer base. Anchor signal: Citroen (automotive) shared this need explicitly; the same conversion-moment pain exists across real estate, healthcare, banking, B2B SaaS, professional services, and F&B.


S2 — Target Users + Personas

PersonaRoleGoalPainWorkaround
B — CS Ops Lead / Bot Manager (Primary)CS Ops Lead or Bot Manager — industry-agnostic.Convert high-intent inbound conversations into booked appointments end-to-end inside the chat."Qualify but don't close" gap. 5-30 min lag → 40-60% drop-off.Manual chat-to-calendar handoff OR custom API Integration.
C — Mekari Implementation Consultant (Primary)Internal Mekari consultant delivering paid implementation across customers.Deliver a working template in onboarding hours.Every onboarding repeats the same Calendar API Integration setup.Custom integration per customer; engineering review per implementation.
A — Qontak Admin (Non-Tech) (Secondary)General Qontak admin at SME/mid-market. Frequently non-technical."I bought Qontak — I expect my AI Agent to talk to my team's Google Calendar without me reading API docs."OAuth + Calendar API via API Integration assumes admin can debug 4xx errors.Most don't attempt; few escalate via support.

Design constraint inherited from Persona A: No knowledge of OAuth scopes, refresh tokens, or Google API mechanics may be required.


S3 — Non-Goals

#Non-GoalWhere deferred
1Other Google Workspace products (Sheets, Drive, Gmail, Docs)Phase 2+
2Other calendar providers (Outlook/365, Apple, Calendly)Phase 4+
3Per-rep / round-robin / per-team calendar assignmentPhase 1.5 or P2
4Reschedule + cancel actions by agentPhase 1.5
5Two-way sync from Google Calendar back to QontakFuture
6Recurring eventsFuture
7Group bookingsFuture
8Customer-facing embedded calendar widgetFuture
9Custom WhatsApp/SMS reminders from QontakCross-channel reminder is its own feature
10Data residency / Google data location complianceCustomer-owned
11AI Agent LLM / runtime behavior changesParent AI Agent runtime PRD

S4 — Constraints

ConstraintSpec
PlatformWeb only — Chrome, Firefox, Edge (latest stable). No mobile app for config.
Performance — OAuth consent flowRedirect to Google + return ≤ 10s p95
Performance — slot lookup≤ 1.5s p95
Performance — create event + send invite≤ 2s p95
Performance — token refresh (proactive)At 50 minutes into 60-minute TTL. Background, non-blocking.
Performance — token refresh (reactive)On 401 mid-call, refresh + retry once ≤ 500ms p95
Performance — concurrent refresh safetySingle-flight per agent_id; ≤500ms wait + reuse
Data — slot lookup windowDefault 7 days; configurable 1-30 days per template
Data — max templates per customer10
Data — max bookings/agent/dayNo soft cap (Google quota: 60 writes/min/user)
Data — credential storage1 access + refresh token pair per AI Agent. Encrypted at rest.
OAuth scopeshttps://www.googleapis.com/auth/calendar.events (least privilege)
OAuth redirect URIhttps://chat.qontak.com/oauth/google/callback (TBD final by Eko)
Plan scopeAI Agent feature + Native Integrations add-on SKU (compound)
Feature flaggoogle_calendar_native_integration_enabled | OFF | per-company

Role Matrix

OperationCANCANNOTUnauthorized behavior
Configure Google Calendar actionAdmin, SpvAgent, Read-only, Billing-onlyTab hidden, no 403
Authorize via OAuth (Google)Customer's Google adminAnyone without those rightsGoverned by Google OAuth
Edit template / re-consentAdmin, SpvOther roles
Delete AI Agent with attached CalendarAdmin, SpvOther roles

S4.7 — Data Lifecycle

ArtifactRetentionCleanup TriggerUser-Visible Effect
OAuth access_tokenTTL ~1hrAuto-refresh via GCAL-S07None
OAuth refresh_tokenLifetime of AI Agent OR until revokedCustomer revokes OR DELETE AI Agent → cascade purgeBanner: "Re-authorize Google Calendar"
OAuth state / nonce10 minTTL on RedisNone
Per-customer template configLifetime of AI AgentDELETE AI Agent → cascade purgeTemplates removed
Booking event metadata cache24hCronNone

S5 — Rollout

StageAudienceDurationWindowGoal
Internal Alpha2 internal accounts1 weekMid-May 2026OAuth + 3 actions + auto-refresh validated. 🎨 MCP Pixel UI review passed by Wulan/Rizky.
Closed BetaCitroen + 2–3 friendly customers TBD4 weeksLate May → end of June 2026Booking failure rate ≤ 3%; ≥1 customer per vertical.
Open BetaAll Native Integrations add-on purchasers3 weeksJuly 2026Booking failure rate ≤ 2% sustained 2 weeks.
GAPlan + add-on. PMM launch.Ongoing — Q3 2026

Feature flag: google_calendar_native_integration_enabled (S4). Backward compatible: existing AI Agents unaffected. Migration: none.


S6 — Observability

#Event NameTriggerProperties
1calendar_template_configuredAdmin creates/edits/deletes templatecompany_id, agent_id, template_id, action, template_count
2calendar_oauth_initiatedAdmin clicks Connectcompany_id, user_id, agent_id, scope_requested
3calendar_oauth_completedGoogle returns auth + token storedcompany_id, agent_id, latency_ms
4calendar_oauth_failedOAuth failscompany_id, agent_id, failure_reason, error_code
5calendar_token_refresh_failedRefresh fails — security signalcompany_id, agent_id, failure_reason
5bcalendar_token_refreshedRefresh succeedscompany_id, agent_id, refresh_type, latency_ms
6calendar_slot_lookupAgent invokes list_slotscompany_id, agent_id, template_id, slot_count_returned, status, latency_ms
7calendar_booking_createdAgent creates event — conversion eventcompany_id, agent_id, template_id, event_id, attendee_count, time_to_book_sec
8calendar_booking_failedcreate_event failscompany_id, agent_id, template_id, failure_reason
9calendar_invite_sentCalendar invite deliveredcompany_id, agent_id, event_id, invite_status

Dashboard owner: Hadiningbot squad (PM: Dimas; Tech Lead: Eko Aprianto)

Alerts

ConditionThresholdRouting
calendar_booking_failed rate5% in 1hrPagerDuty — Hadiningbot
calendar_oauth_failed rate10% in 1hrSlack #hadiningbot-alerts
calendar_token_refresh_failed per company5% per company in 24hrSlack #hadiningbot-alerts
Slot lookup p95 latency1.5s sustained 30minSlack #hadiningbot-alerts
Booking p95 latency2s sustained 30minSlack #hadiningbot-alerts

S6.5 — Monitoring Cadence

Weekly review for first 4 weeks post-GA, then monthly. Owner: Dimas + Eko. Focus: booking funnel (6 → 7), failure rate, token refresh per company, time-to-book.

S6.6 — BE Architecture (mirrors mekari_qontak_crm per RFC Google Sheets Workflow Nodes)

Google Calendar BE infrastructure follows the canonical pattern already used by mekari_qontak_crm. This subsection lists the new + modified files and the deliverables owned by the BE team. The pattern itself is defined in the RFC — this PRD does NOT duplicate the pattern content, only declares which files mirror it.

New files (Phase 1):

FileMirrorsPurpose
app/core/repositories/node_executions/nodes/google_calendar/execute.rbRepositories::NodeExecutions::Nodes::MekariQontakCrm::ExecuteExecutor for the Google Calendar booking action. Uses process_path for path interpolation.
app/core/repositories/node_resources/google_calendar/calendar_http_client.rbMekariQontakCrm::CrmHttpClient (#request_with_auth)HTTP client wrapping every Google Calendar API call. Implements refresh-on-401 via https://oauth2.googleapis.com/token.
app/core/repositories/node_resources/google_calendar/lookup_resources.rbRepositories::NodeResources::MekariQontakCrm::LookupResourcesExposes resource_key='calendar' — dropdown source for the Calendar field in Screen 3 Action details form.
db/seeds/google_calendar_nodes.rbRFC §6 Sheets seed examplesIdempotent seed for one node_registries row (Calendar booking action) with full properties JSON.
Specs (rspec)mekari_qontak_crm specsCover: executor happy path, lookup happy path, refresh-on-401, seed idempotency.

Modified files (Phase 1):

FileModification
app/core/repositories/node_executions/node_type_registry.rbRegister 'google_calendar' => 'GoogleCalendar::Execute'
app/api/frontend_service/v1/node_resources/use_cases/lookup_resources.rbAdd GoogleCalendar to the PROVIDER_MODULES constant

Deliverable owned by BE team (follow-up RFC): The exact node_registries.properties JSON for the Calendar booking action is a follow-up RFC draft owned by Eko + PM, mirroring RFC §6 Sheets format. The PRD records that the schema follows the canonical pattern — exact field-by-field JSON does NOT need to be published inline in this PRD. The 8 form fields (calendar_id, duration_minutes, lookup_window_days, event_title_pattern, location, event_description, attendees_customer_email_field, attendees_always_cc) are specified in Wireframes v4.2 Screen 3 and are the source of truth for the properties schema.


S6b — Success Metrics

CategoryMetricDefinitionBaselineTarget
ConversionBooking conversion rate inside chat(booking_created / triggered conversations) × 10040-60% via handoff (Assumption)≥65% within 60d of GA
ConversionTime-to-bookp50 from first slot_lookup → event_idSeveral minutes to hours≤30 seconds median
Adoption% customers configuring ≥1 template within 60d(customers w/ template_configured / add-on customers) × 1000%≥50% within 60d
AdoptionAvg bookings per active customer per weekMean per company_idN/A≥20/week within 90d
QualityBooking success ratecreated / (created + failed) × 100N/A≥98% within 60d
QualityOAuth completion rateoauth_completed / oauth_initiated × 100N/A≥85% within 60d
EfficiencyImplementation hours saved per onboardingSelf-reported, n=10TBD (S9 #4)≥6 hours saved

S6c — Stage Gates

StageAudienceDurationSuccess GateOwner
Internal Alpha2 internal accounts1 week0 P0/P1 · End-to-end OAuth + 3 actions + auto-refresh by all 3 personas · booking_failed rate = 0% (n≥10) · All 10 S6 events firing · 🎨 MCP Pixel UI review passed by Wulan/RizkyPM + QA + Eko + Wulan/Rizky
Closed BetaCitroen + 2-3 friendlies4 weeksBooking success ≥ 95% · ≥1 customer per vertical · OAuth completion ≥ 75% · No P0/P1PM + CSM
Open BetaAll add-on purchasers3 weeksbooking_failed ≤ 2% sustained · Conversion ≥ 55% sustained · ≥10 customers configured · Time-to-book ≤ 30 secEko + PM
GAAll eligibleOngoing — Q3 2026All Open Beta gates sustained 2 wks · PMM launch approved · Implementation team trainedPM + PMM (Yosephine Dhisaclara) + Implementation Lead

S7 — Dependencies

#DependencyOwning TeamDeliverableBlocking?
1Google Cloud Console OAuth clientEko / Platform EngNew OAuth 2.0 client ID with calendar.events scopeYES
2Google Sensitive Scope verificationEko + Google reviewApproved before Open Beta. Test mode (100-user) for Closed Beta.YES (Open Beta+); NO (earlier)
3Encrypted token storageHadiningbotSecret store wired into AI Agent recordYES
4Qontak AI Agent config UI extensionHadiningbotConfig page extendedYES
5Native Integrations add-on SKUDevina + Sales OpsSKU + entitlement API. ⚠️ Not Started (Risk #12)YES (Open Beta); NO (Internal Alpha)
6Feature Flag servicePlatform Enggoogle_calendar_native_integration_enabled registeredNO
7Qontak RBACPlatform EngRole identification in sessionNO
8Implementation team trainingImplementation Lead + DevinaInternal docs + per-vertical templatesYES (GA only)

Graceful degradation: Runtime fallback is owned by parent AI Agent runtime layer. This PRD emits calendar_booking_failed with failure_reason so upstream can detect and fallback.


S8 — Decisions + Alternatives Rejected

S8a — Decisions Made

DateDecisionRationale
2026-05-06Phase 1 = Google Calendar (not Sheets)Stronger demo + OAuth reusable for Phase 2 Sheets
2026-05-06OAuth scope = calendar.eventsLeast privilege; smaller Google verification scope
2026-05-06Single shared calendar per company in P1Per-rep deferred
2026-05-063 actions: list_slots + create_event + send_inviteTight closed loop
2026-05-06Per-customer template config, max 10Supports multi-vertical
2026-05-06Slot window default 7 days, configurable 1-30Industry-agnostic
2026-05-06No soft cap on bookings/agent/dayAligned with user SOP
2026-05-06Compound entitlementMatches Mekari Action
2026-05-06Single Native Integrations SKU covers all providersSimpler upsell
2026-05-06Web only — no mobile configAll personas configure on web
2026-05-06One-way write onlyWebhook listener = separate architecture
2026-05-06Industry-agnostic framingQontak's ICP is cross-industry
2026-05-06Standard rollout namingMatches Mekari Action
2026-05-06Primary KPI = 65% conversionRealistic improvement target
2026-05-07OAuth token refresh = proactive + reactive + single-flightDaily-use feature cannot tolerate hourly silent failures
2026-05-07UI generated via MCP Pixel, no FigmaFirst AI SDLC initiative
2026-05-22BE infrastructure mirrors mekari_qontak_crm per RFC Google Sheets Workflow Nodes. Credential storage canonical = organization_connections table (code='google_calendar', auth_type='oauth2', auth_data = lockbox-encrypted JSON of access/refresh/expires_at). Refresh-on-401 canonical = MekariQontakCrm::CrmHttpClient#request_with_auth pattern with token endpoint https://oauth2.googleapis.com/token. Single credential per org per provider for v1. Credential resolved by HTTP client using org_id + provider code, NOT a user-facing property. Calendar dropdown lookup follows resource_types pattern with provider='google_calendar', resource_key='calendar'.Reusing an existing, proven BE pattern (already used by mekari_qontak_crm) reduces implementation risk and standardizes the credential layer across all native integrations (Calendar, Sheets, Salesforce, Zoho).

S8b — Alternatives Rejected

DateAlternativeWhy Rejected
2026-05-06Phase 1 = Google SheetsBooking is stronger demo
2026-05-06Calendar.full OAuth scopeOver-broad
2026-05-06Per-rep/round-robin in P1~3x effort
2026-05-06Reschedule + cancel in P1Phase 1.5
2026-05-06Two-way syncInfrastructure not in scope
2026-05-06Recurring eventsNot worth P1
2026-05-06Customer-facing embedded calendar widgetChat-native faster
2026-05-06Custom WhatsApp/SMS remindersCross-channel = own feature
2026-05-06Single-template-per-customerReal estate needs multi-template
2026-05-06"Test Drive Booking" namingToo automotive-specific
2026-05-06Per-provider SKUFragments pricing
2026-05-06Free for any AI Agent customerLeaves demand on table
2026-05-0675% conversion target65% is realistic
2026-05-06Cross-product upsell metric in S6bTrack informally
2026-05-07Reactive-only token refresh200-500ms latency spike every 60min
2026-05-07Manual Figma design handoffFirst AI SDLC initiative

S9 — Open Questions / Risks

#TypeQuestion / RiskOwnerDeadlineMitigation
1Open QFinal OAuth redirect URIEkoBefore Internal Alpha
2Open QGoogle Cloud Console project — new or extend?Eko + PlatformBefore Internal Alpha
3Open QClosed Beta customer selectionDimas + CSMEnd of Internal Alpha
4Open QImplementation Consultant baseline hoursDevina + Implementation Lead2026-06-15
5Open QEpic keyDimasBefore Internal Alpha
6Risk 🎨MCP Pixel output quality not yet validatedWulan/Rizky + EkoEnd of Internal AlphaInternal Alpha UI review gate. Fallback to manual UI build in Phase 1.5.
7AssumptionBaseline 40-60% drop-offDimas + CitroenEnd of Closed Beta
8RiskGoogle Sensitive Scope verification 2-6 weeksEko + PlatformBefore Open BetaTest mode covers Closed Beta. Submit by 2026-05-15.
9RiskCustomer's Google admin can revoke OAuthEkoAt GAS6 #5 detects; banner Re-authorize; graceful agent fallback.
10RiskGoogle Calendar API quota (60 writes/min/user)EkoAt GAGoogle's quota-increase form; token bucket per customer.
11RiskSingle shared calendar limitPMBefore Closed BetaCustomer docs; accelerate Phase 1.5 if 2+ customers push.
12RiskNative Integrations add-on SKU not startedDevina + Sales Ops2026-06-15Internal Alpha + Closed Beta hardcode bypass. SKU before Open Beta.
13Open QFrontend confirms select + slider + textarea HTML elements exist in the design system for the Calendar booking form per node_registries.properties schema (mirrors RFC §10 question for Sheets).Wulan + RizkyBefore Internal Alpha BE seed merge
14Open QOAuth scope userinfo.email may be needed in addition to calendar.events to render the connected Google email in the N2 inline OAuth status row's connected state. PM/BE to confirm.Dimas + EkoBefore Internal Alpha
15Open QSingle credential per org per provider for v1 (no per-team or per-AI-agent credential isolation in Phase 1). Mirrors RFC §3 Out-of-scope for Sheets. PM confirmed in Native Integrations ANCHOR S8a 2026-04-27 ("1 credential per AI Agent"). Flag: the ANCHOR's "per AI Agent" framing and the RFC's "per org per provider" framing are not identical — needs follow-up alignment between PM + Eko + ANCHOR owner before Internal Alpha to confirm v1 ships with the org-level credential and the ANCHOR's "per AI Agent" framing is treated as a future-phase enhancement (or vice versa).Dimas + EkoBefore Internal Alpha

🎨 S11 — UI Specification — DESIGNER REVIEW REQUIRED

🎨 Wulan + Rizky: This entire section is your review surface. Canonical source of truth: Wireframes + MCP Pixel Prompts v4.2 (Confluence rev 6, Figma 2026-05-22). All IA, drawer states, and per-screen MCP Pixel prompt blocks live there. This section summarises the IA contract and the verification tracker.

v1.8 — IA correction (2026-05-22) [carried forward through v1.10 — canonical source is Wireframes v4.2]. Aligns with Native Integrations ANCHOR v1.2 and Mekari Action ANCHOR v1.3 S8a decisions of 2026-05-22. Cancels the v1.6/v1.7 invention of a new "Native integrations" sibling group. Native rows are additional rows inside the existing Other integration group.

§UI Approach — Existing Figma Reuse, native row under Other integration

Per PM decision (a): one booking template = one attached action. No separate Google Calendar config surface. No multi-template management screen. No TemplateEditorModal. No standalone CalendarStatusBanner. Calendar concepts (calendar picker, duration, lookup window, attendees) collapse into the single existing per-action details form schema that every other Mekari Qontak Bot action uses — generated by MCP Pixel from a prose prompt, no Figma node required.

FieldValue
FeatureGoogle Calendar action — In-conversation booking
URL/chatbot/ai-agent/{agent_id}/config
AccessAdmin, Supervisor (per S4)
Design sourceExisting Figma reuse for drawer (E2–E7) + per-action details form via MCP Pixel (Pixel MCP-only spec, no Figma node)
Canonical wireframesWireframes v4.2 — Figma 2026-05-22

§Action drawer — canonical IA (Figma 2026-05-22)

The right-side single-column Action drawer contains 5 groups, in this fixed order:

#GroupPhase 1 contribution
1Mekari Qontakunchanged
2Mekari Talentaunchanged
3Mekari Jurnalunchanged
4Mekari Destyunchanged
5Other integrationPhase 1 adds exactly one row: Google Calendar. Future phases add Sheets, Salesforce, Zoho as additional rows in this same group — never as new groups.

Hard NOs (do not regress to these — flagged here because v1.6/v1.7 of this PRD got them wrong):

  • ❌ Do NOT add a new top-level group called "Native integrations" between Mekari Desty and Other integration.
  • ❌ Do NOT nest native rows under a "Mekari Action" namespace.
  • ❌ Do NOT introduce a two-column drawer with an internal preview panel (logo + name + description + [Configure] CTA + "Coming soon" rail). The drawer is single-column, right-side.
  • ❌ Do NOT use MpIcon name="calendar" or any placeholder component name in MCP Pixel prompts (code-gen treats placeholders as instructions).

§Drawer states — 4 canonical P0 states (from Figma 2026-05-22)

StateDescriptionSource
Collapse / expand (default)Groups render in fixed order. Each group header is collapsible. Other integration shows the Google Calendar row in Phase 1.Figma 2026-05-22
ScrolledLong-list scroll behavior inside the single-column drawer; group headers can sticky per Mekari Pixel pattern.Figma 2026-05-22
Search foundFlat list across all 5 groups — group headers suppressed. Each result row carries its brand icon.Figma 2026-05-22
Search not foundCentered message: "{query}" not found / Recheck the keywords you have typed and try searching again.Figma 2026-05-22

Runtime-only states (P1, V8 pending — NOT in canonical Figma)

StateStatusNotes
LoadingP1 — V8 pendingSkeleton rows while drawer fetches groups + rows. Not in canonical Figma; pending design pass before Open Beta.
Failed-to-loadP1 — V8 pendingDrawer-level error + Retry. Not in canonical Figma; pending design pass before Open Beta.

§Screens — what is Figma-canonical vs MCP Pixel-only

ScreenSourceNotes
Screen 0 — Actions tab empty/populatedExisting Figma (E2/E3)Reused as-is.
Screen 1 — Action drawer (4 states above)Canonical Figma 2026-05-22 (V1 ✅ RESOLVED)Single-column right-side drawer. Wireframes v4.2 carries the prompt block.
Screen 2 — Search found / Search not foundCanonical Figma 2026-05-22See drawer states.
Screen 3 — Per-action details form (the screen rendered after clicking Google Calendar in the drawer)Pixel MCP-only spec — no Figma node required.The MCP Pixel prompt IS the spec. This is the form surface where the admin completes the calendar template (calendar picker, duration, lookup window, event title pattern, attendees mapping, OAuth status row) — all generated from prose. V2 dropped for this screen.
Screen 4 — OAuth modal / connect overlayPixel MCP-only specPrompt block in Wireframes v4.2.
Screen 5 — Connection-Lost banner + Re-authorizePixel MCP-only specPrompt block in Wireframes v4.2.
Screen 6 — Cascade delete confirmation modalExisting modal + Calendar-specific copyPrompt block in Wireframes v4.2.

§Brand asset guard (HARD RULE — do not violate)

Every brand SVG slot in any MCP Pixel prompt — including the Google Calendar row in the drawer, the brand glyph in Screen 4 OAuth modal, and any other native row added by future phases — must be authored as:

[BRAND_SVG_PENDING_V4 — DO NOT CODE-GEN WITHOUT REPLACING]

Code-gen of any prompt containing a BRAND_SVG_PENDING_V4 marker is forbidden until V4 is closed. Never substitute placeholder component names such as MpIcon name="calendar", MpIcon name="google", <icon>, or similar — code-gen treats placeholders as literal instructions and silently ships the wrong asset. Wulan + Rizky own V4 closure (Google Calendar brand SVG path + hex colors). On V4 closure, replace every [BRAND_SVG_PENDING_V4 …] marker with the actual inlined SVG, then code-gen can proceed.

§Per-action OAuth status row — state machine (lives inside Screen 3 form)

StateVisualAction
not-connectedBrand glyph ([BRAND_SVG_PENDING_V4 — DO NOT CODE-GEN WITHOUT REPLACING]) + body "Connect to Google Calendar to activate this action"Primary [Connect Google Calendar] button right-aligned
connectedBrand glyph + MpAvatar (user's Google photo if avail) + email + success checkTertiary [Re-authorize] + [Disconnect] right-aligned
connection-lostMpBanner variant="danger" — "Connection lost — bookings paused."Primary [Re-authorize Google Calendar]
quota-exceededMpBanner variant="warning" — "Google API quota exceeded — bookings may be delayed."Tertiary [View quota usage] + primary [Request quota increase]

Outstanding verifications (V1–V8) — see Wireframes v4.2 for full tracker

#VerificationOwnerStatus
V1Canonical drawer container pattern (single-column right-side; 5 groups, Other integration is the 5th)Wulan + RizkyRESOLVED 2026-05-22 — Figma confirms single-column right-side drawer; native row lives under Other integration.
V2Per-screen Figma node IDs (Screens 0–2 + sub-screens; Screens 3–5 = Pixel MCP-only, no Figma node)Wulan + RizkyOpen for Screens 0–2 only. Screen 3 explicitly dropped (Pixel MCP-only spec).
V3Live Mp* prop/slot names from @mekari/pixel3 — Pixel API verification via https://ai.mekari.design/mcp (Mekari's canonical design-system MCP)FE + Wulan/RizkyOpen
V4Google Calendar brand SVG path + hex colors — closes the [BRAND_SVG_PENDING_V4] guardWulan + RizkyOpen — blocks code-gen
V5Empty-state illustration asset (Screen 0)Wulan + RizkyOpen
V6Microcopy review — all literal strings flagged engineering-draftContent Design + YosephineOpen
V7Locale set (id-ID + en)PMOpen
V8Drawer runtime states (Loading, Failed-to-load) — design pass before Open BetaWulan + RizkyNEW — Open

References: Canonical IA + drawer states + per-screen MCP Pixel prompts → Wireframes v4.2. IA contract + S8a decisions → Native Integrations ANCHOR v1.2 and Mekari Action ANCHOR v1.3.


S12 — API & Webhook Behavior

#BehaviorTriggered byExpectedFailure
1Initiate OAuthAdmin clicks ConnectQontak returns Google OAuth URL with state + scopeState mismatch / user cancel → calendar_oauth_failed
2Exchange auth code for tokensGoogle redirect with ?code=...&state=...Validate state. Tokens encrypted + bound to agent_id. calendar_oauth_completed.Storage error → 500 + ops alert
3Refresh access token — proactive + reactiveProactive: BG job at 50min TTL. Reactive: Google 401 mid-call.POST token endpoint. Single-flight per agent_id. Atomic replacement. calendar_token_refreshed.Refresh fails → calendar_token_refresh_failed fires; banner → Connection Lost; cascade to GCAL-S05.
4List available slotsAgent invokes list_slots(template_id, optional_date_hint)Decrypt token. GET freeBusy. Compute open slots. calendar_slot_lookup fires.5xx → status=failed. 429 → backoff.
5Create calendar event + inviteAgent invokes create_event(template_id, slot_start, customer_data)POST events with sendUpdates=all. Google sends invites. calendar_booking_created + calendar_invite_sent fire.5xx → calendar_booking_failed. 401 → refresh + retry. 429 → backoff. Conflict → slot_conflict.
6Cascade delete on AI Agent deletionAdmin deletes AI AgentDelete agent. Purge encrypted tokens. Purge templates. Tokens on Google's side NOT revoked.Partial cascade → log alert; orphan purged by nightly cron
7Disconnect (without deleting agent)Admin clicks DisconnectPurge tokens locally. Banner → Not Connected. Templates remain.Storage error → log alert

S12.1 — Credential Storage & OAuth (BE-canonical, per RFC Google Sheets Workflow Nodes)

Google Calendar credentials follow the canonical pattern already used by mekari_qontak_crm. Encoded BE-canonical values below.

A. Credential storage

PropertyValue
Tableorganization_connections
category'credential'
code'google_calendar'
auth_type'oauth2'
auth_dataLockbox-encrypted JSON { access_token, refresh_token, expires_at }. Never logged.
OAuth scopeshttps://www.googleapis.com/auth/calendar.events (narrow). If user email is required to render in the N2 inline OAuth status row's connected state, also https://www.googleapis.com/auth/userinfo.email (PM to confirm — flagged as Open Question in S9).
Token endpointhttps://oauth2.googleapis.com/token
Per-org cardinalitySingle credential per org per provider (v1). One row in organization_connections per org with code='google_calendar'. Re-connecting overwrites the existing row in place.
ResolutionCredential is resolved by the HTTP client using org_id + provider code. The credential is NOT a user-facing field — the user does NOT pick "which credential" in the form. The N2 inline OAuth status row in Wireframes v4.2 Screen 3 IS the credential UI.

B. HTTP client (refresh-on-401)

New file: app/core/repositories/node_resources/google_calendar/calendar_http_client.rb.

Mirrors MekariQontakCrm::CrmHttpClient#request_with_auth. Refresh-on-401 behavior:

  1. On any 401 from Google Calendar API, attempt token refresh via POST https://oauth2.googleapis.com/token with grant_type=refresh_token + stored refresh_token.
  2. On refresh success: update organization_connections.auth_data with new access_token + new expires_at. Retry the original request once.
  3. On refresh failure (invalid_grant, network error, scope revoked, etc.): mark credential as connection-lost, trigger N2 row state change, surface to user via the Wireframes v4.2 Screen 3 N2 inline OAuth status row (connection-lost state — see GCAL-S05).

C. Quota

Google Calendar API daily quota: ~1,000,000 requests/day per project, ~600 req/min/user (varies; verify in GCP console). Document in BE runbook. Surface quota-exceeded state through the N2 inline OAuth status row (already specced in Wireframes v4.2 Screen 3 N2 state machine — see S11 §Per-action OAuth status row state machine).


S13 — System Flow + User Stories + ACs

Flow

FieldValue
Flow NameConfigure Google Calendar action + Agent books an appointment end-to-end
TypeUser Journey (admin setup) + API Sequence (runtime booking)
#Step
1Admin/Spv opens AI Agent configuration page
2Admin/Spv clicks [+ Add Action] → ActionPickerDrawer opens
3Admin/Spv scrolls drawer to existing Other integration group → clicks Google Calendar row
4Per-action details form (Screen 3, Pixel MCP-only spec) opens with inline OAuth status row in "Not Connected" state
5Admin/Spv clicks [Connect Google Calendar] → redirected to Google OAuth consent
6Customer's Google admin approves calendar.events scope
7Google redirects with auth code; Qontak exchanges for tokens
8Inline OAuth status row flips to "Connected"
9Admin/Spv fills the per-action details form (calendar picker, duration, lookup window, event title pattern, attendees mapping) → saves
10At runtime: customer chats → Agent invokes list_slots
11Agent presents slots; customer picks one
12Agent invokes create_event → Google sends invite
13Agent confirms booking; events fire
14a-eFailure branches (see story Error scenarios)

Story Index

Story IDTitlePriorityDependenciesDesigner review?
GCAL-S01 🎨Connect AI Agent to Google Calendar via OAuthMust HaveNoneYes — UI
GCAL-S02 🎨Configure a Calendar templateMust HaveS01Yes — UI
GCAL-S03Agent looks up available slotsMust HaveS02No — backend
GCAL-S04Agent creates calendar event + sends inviteMust HaveS03No — backend
GCAL-S05 🎨Handle token refresh failure / re-authorizeShould HaveS01Yes — UI
GCAL-S06 🎨Cascade delete AI Agent + tokens + templatesMust HaveS02Yes — UI (confirmation modal)
GCAL-S07Auto-refresh OAuth access tokenMust HaveS01No — backend
GCAL-S08Backend: node_registries seed + executor + HTTP client + lookup resourcesMust HaveNone (BE infra)No — backend
GCAL-S09Feature flag rollout: FEATURE_GOOGLE_CALENDAR_NODESMust HaveS08No — backend

🎨 Story GCAL-S01 — Connect AI Agent to Google Calendar via OAuth

🎨 Designer review: Yes — frontend story. Wulan/Rizky review the UI States and Permission Model below; the full MCP Pixel prompt is in the Wireframes page → Screen 1 + Screen 2 + Screen 7.

FieldValue
PriorityMust Have — entry point for the entire feature
Before stateAI Agent has no Google Calendar action. No OAuth tokens.
After deltaGoogle Calendar row appears in the existing Other integration group of the action drawer. Admin clicks it → per-action details form (Screen 3) opens with inline OAuth status row in Not Connected. Admin clicks Connect → Google OAuth → tokens stored encrypted, bound to agent_id. OAuth status row flips to Connected.
User storyAs an Admin/Supervisor at a Qontak customer with the Native Integrations add-on, I want to connect my AI Agent to a Google Calendar via a one-time OAuth flow, so that my agent can read availability and create bookings on my team's calendar.
UI ReferenceS11 §Action drawer (Screen 1) + §Per-action OAuth status row state machine (lives inside Screen 3 form). Screens 3–4 are Pixel MCP-only specs (no Figma node).

Acceptance Criteria (ATDD / Gherkin)

Scenario: AC-1 — Google Calendar row is visible in Other integration group when entitlement is complete
Type: Constraint — feature flag + add-on entitlement

Given the user is Admin or Supervisor at a company where
google_calendar_native_integration_enabled = ON
AND the company has Native Integrations add-on SKU active
When the user opens the Action drawer
Then the existing "Other integration" group renders a "Google Calendar" row
(alongside any pre-existing rows in that group)
And clicking the row opens the per-action details form (Screen 3) with the inline OAuth status row in Not Connected state
And no new top-level group (such as "Native integrations") is rendered
Scenario: AC-2 — Initiate OAuth from Not Connected state
Type: Happy path

Given the user is on the per-action details form (Screen 3) with the inline OAuth status row in Not Connected state
When the user clicks [Connect Google Calendar]
Then Qontak generates a unique OAuth state nonce
And stores it server-side with 10-minute TTL
And redirects the user to Google OAuth consent URL with
scope = "https://www.googleapis.com/auth/calendar.events"
state = <nonce>
redirect_uri = <configured>
And calendar_oauth_initiated event fires with company_id, user_id, agent_id, scope_requested
Scenario: AC-3 — Exchange auth code for tokens
Type: Happy path

Given Google redirects the user back to /oauth/google/callback?code=<code>&state=<nonce>
When Qontak receives the callback
Then it validates the state nonce matches the stored value (CSRF protection)
And POSTs to Google's token endpoint with the auth code
And receives access_token + refresh_token
And encrypts both at rest, binding them to agent_id
And calendar_oauth_completed event fires with latency_ms
And the user is redirected back to agent config with success state
And the inline OAuth status row flips to Connected
Scenario: AC-4 — State mismatch rejected (CSRF protection)
Type: Edge case — security

Given an OAuth callback arrives with state nonce that doesn't match any stored value
When the callback handler runs
Then the request is rejected with HTTP 400
And an error page shows: "Authorization failed (security check). Try connecting again."
And calendar_oauth_failed event fires with failure_reason = "state_mismatch"
Scenario: AC-5 — User denies consent at Google
Type: Edge case — user cancel

Given the user reaches Google's consent screen
When the user clicks Cancel
Then Google redirects to Qontak with ?error=access_denied
And the inline OAuth status row stays in Not Connected state
And a modal shows: "Connection cancelled. You can try again whenever you're ready."
And calendar_oauth_failed event fires with failure_reason = "user_denied"
Scenario: AC-6 — Role gate enforcement
Type: Constraint — role gate (per S4 Read/Write)

Given the user's role is NOT Admin or Supervisor (e.g., Agent, Read-only, Billing-only)
When the user views AI Agent config
Then the Action drawer is not reachable (entry CTA hidden)
And the [Connect Google Calendar] button is not reachable
And no 403 is surfaced to the user

Errors / Unhappy Path

Scenario: ERR-1 — Qontak fails to generate OAuth URL
Given the user clicks [Connect Google Calendar]
When Qontak's call to generate the OAuth URL fails (config error, Redis down)
Then a modal shows: "Could not start authorization. Try again." with a [Retry] button
And calendar_oauth_failed event is logged with failure_reason = "err_oauth_url"
Scenario: ERR-2 — Google returns 400 on token exchange
Given Qontak receives the auth code from Google
When the token exchange POST returns HTTP 400 (invalid code, redirect mismatch, scope error)
Then a modal shows: "Authorization failed. Verify your Google account and try again."
And calendar_oauth_failed fires with failure_reason = "err_token_exchange"
Scenario: ERR-3 — Token storage / encryption fails
Given tokens are received from Google
When the encrypt + store step fails
Then HTTP 500 is returned and an ops alert fires
And the banner stays in transitional state
And the user sees: "Could not save authorization. Ops has been notified. Try again."
And calendar_oauth_failed fires with failure_reason = "err_token_storage"

Backend implementation cue (BE-canonical, per RFC Google Sheets Workflow Nodes; see also S12.1): On handshake success, persist as a row in organization_connections with org_id=<current_org>, category='credential', code='google_calendar', auth_type='oauth2', auth_data=<lockbox-encrypted {access_token, refresh_token, expires_at}>. If a row already exists for (org_id, 'google_calendar'), update it in place (single credential per org per provider in v1). Token endpoint = https://oauth2.googleapis.com/token. Credential is resolved by the HTTP client using org_id + provider code — NOT a user-facing property. Does NOT change Gherkin AC scope; user-facing assertions above stand as-is.

AspectDetail
Permission — CANAdmin, Supervisor
Permission — CANNOTAgent, Read-only, Billing-only
Permission — UnauthorizedAction drawer entry CTA hidden; Google Calendar row in Other integration group not reachable
UI — Empty (Not Connected)"Connect to Google Calendar to enable in-conversation booking." + [Connect Google Calendar]
UI — Loading (Connecting)Spinner + "Redirecting to Google for authorization…"
UI — ErrorModal error + Retry
UI — Success (Connected)Green check + "Connected as [admin_email]" + [Re-authorize] / [Disconnect]
FieldTypeRequiredSource
company_idstring (UUID)YesAuth session
user_idstringYesAuth session
agent_idstringYesURL param
state_noncestring (random)YesServer-generated, 10-min TTL
auth_codestringYesGoogle callback param
access_tokenstring (sensitive)YesEncrypted at rest
refresh_tokenstring (sensitive)YesEncrypted at rest
connected_google_emailstringYesGoogle's userinfo

🎨 Story GCAL-S02 — Configure a Calendar template

🎨 Designer review: Yes — frontend story. This is the largest form surface in P1. Wulan/Rizky review the Action details form (Screen 3, extends E7 — Pixel MCP-only spec) including the Calendar dropdown field, all MpFormControl-wrapped inputs, validation states, the slider behavior, and the attached-action row in the populated Actions tab (Screen 1, extends E3). Full MCP Pixel prompt: Wireframes v4.2 → Screen 3.

FieldValue
PriorityMust Have — defines the booking shape per use case
Before stateOAuth Connected (N2 inline OAuth status row = connected inside the Action details form); no booking templates attached yet — Actions tab shows the empty state (Screen 0, extends E2).
After deltaAdmin selects Google Calendar from the Action drawer (Other integration group) → Action details form (Screen 3, extends E7) opens. Admin configures name, calendar (via Calendar dropdown field inside the form), duration, lookup window, event title pattern, location, description, attendees mapping. On Save, one booking template = one attached action row in the Actions tab populated state (Screen 1, extends E3). Max 10 attached Calendar actions per customer (S4 constraint). calendar_template_configured fires.
User storyAs an Admin/Supervisor, I want to attach one or more Google Calendar booking actions (e.g., 30-min test drive, 60-min property viewing, 15-min demo call) so that my agent uses the right defaults for each kind of booking in my industry.
UI ReferenceS11 §Screen 3 — Action details form (extends E7, Pixel MCP-only spec). Calendar dropdown field = MpFormControl label="Calendar" + MpSelect populated from Google Calendar Lists API. N2 inline OAuth status row inside the form's Form configuration block. Attached actions surface as rows in the Actions tab populated state (Screen 1, extends E3) — no separate template-list management surface.

Acceptance Criteria (ATDD / Gherkin)

Scenario: AC-1 — Open the Action details form for a new Google Calendar action
Type: Happy path

Given OAuth is Connected (N2 inline OAuth status row = connected)
And no Google Calendar action is attached yet
When the user opens the Action drawer and selects `Google Calendar` under `Other integration`
Then the Action details form (Screen 3, extends E7) opens with empty fields
And the Calendar dropdown field is populated by calling Google Calendar Lists API
Scenario: AC-2 — Save a new Calendar action
Type: Happy path

Given all required fields are filled in the Action details form (name, calendar_id, duration, event_title_pattern, attendee_mapping)
When the user clicks [Save]
Then the template is persisted server-side as one attached action
And a new attached action row appears in the Actions tab populated state (Screen 1, extends E3)
And calendar_template_configured event fires with action="created" and template_count
Scenario: AC-3 — Reject unknown placeholders in event title
Type: Edge case — validation

Given the event title pattern contains an unknown placeholder, e.g. {{undefined_var}}
When the user clicks [Save Template]
Then an inline validation error shows under the field:
"Unknown placeholder. Valid options: {{customer_name}}, {{vehicle}}, {{lead_source}}, {{phone}}, {{email}}"
And the [Save Template] button is disabled until the error is fixed
Scenario: AC-4 — Lookup window bounds
Type: Edge case — boundary

Given the user adjusts the lookup window slider
When the slider value would be 0 or > 30 days
Then the slider physically clamps to the 1-30 range
And no error is shown (prevention by control)
Scenario: AC-5 — Attached-action count limit (S4 constraint)
Type: Boundary — quota

Given the customer already has 10 Google Calendar actions attached to the AI Agent
When the user opens the Action drawer and tries to select `Google Calendar` again
Then the row is disabled in the drawer
And a tooltip shows:
"Maximum 10 Google Calendar actions per customer. Delete an unused action to add a new one."
Scenario: AC-6 — Edit an existing Calendar action
Type: Happy path

Given an attached Calendar action exists in the Actions tab populated state (Screen 1, extends E3)
When the admin clicks the action row to open it
Then the Action details form (Screen 3, extends E7) opens pre-filled with the action's values
And on Save, calendar_template_configured fires with action="edited"
Scenario: AC-7 — Delete a Calendar action
Type: Happy path

Given an attached Calendar action exists in the Actions tab populated state
When the user clicks Delete on the action row and confirms in the modal
Then the attached action is removed from the Actions tab
And calendar_template_configured fires with action="deleted"

Errors / Unhappy Path

Scenario: ERR-1 — Calendar dropdown field fails to load Google calendars
Given the user has opened the Action details form (Screen 3, extends E7)
When the Calendar dropdown field fails to load (Google 5xx)
Then the dropdown shows: "Could not load your calendars. Try again." with a [Retry] button
Scenario: ERR-2 — Save fails (Qontak storage error)
Given the user clicks [Save] in the Action details form
When Qontak's storage write fails
Then the form shows: "Could not save action. Try again." with a [Retry] button
And the error is logged
Scenario: ERR-3 — Template deleted mid-booking
Given an active conversation is mid-booking using template_id = X
When template X is deleted in another tab
Then per S3 #5, runtime booking failure is handled by the agent (parent runtime PRD scope)

Backend implementation cue (BE-canonical, per RFC Google Sheets Workflow Nodes; see also S6.6 + S12.1): The Calendar dropdown is sourced via Repositories::NodeResources::GoogleCalendar::LookupResources with resource_key='calendar'. Underlying call: GET https://www.googleapis.com/calendar/v3/users/me/calendarList?fields=items(id,summary,primary). Each item returned as {name: summary, value: id}. Primary calendar marked with a badge / sorted to the top. Lookup is only called when the N2 inline OAuth status row inside Screen 3 is in connected state. The calendar_id field in node_registries.properties declares is_rl=true, resource_types=[{provider: "google_calendar", resource_key: "calendar"}] per the canonical RFC properties shape. Does NOT change Gherkin AC scope.

AspectDetail
Permission — CANAdmin, Supervisor
Permission — CANNOTOther roles
UI — Empty (Actions tab)Screen 0 (extends E2) — empty state with prompt to attach an action via the Action drawer
UI — Loading3 skeleton rows in the Actions tab populated state (Screen 1, extends E3)
UI — Error"Could not load actions. Try again." + Retry
UI — SuccessAttached-action rows in Screen 1 (extends E3); Google Calendar row in the Action drawer disabled at 10 attached
FieldTypeRequiredSource
template_idstring (UUID)YesServer-generated
namestringYesUser input
calendar_idstringYesCalendar dropdown field (Screen 3 Action details form)
duration_minutesintegerYesUser input
lookup_window_daysinteger (1-30)YesUser input
event_title_patternstring (with placeholders)YesUser input
locationstringNoUser input
description_patternstring (with placeholders)NoUser input
attendee_mappingobjectYesUser input

Story GCAL-S03 — Agent looks up available slots in a conversation

No designer review needed — backend behavior; status reflected via N2 inline OAuth status row inside Screen 3 (covered by GCAL-S05 review).

FieldValue
PriorityMust Have — first runtime touch in the closed loop
Before stateOAuth Connected + ≥1 template configured. No runtime action invoked yet.
After deltaAgent invokes list_slots(template_id, optional_date_hint). Qontak decrypts token (refreshes if needed per S07), calls Google freeBusy, returns aligned slots.
User storyAs the AI Agent at runtime, I want to look up available slots in the configured Google Calendar within the configured lookup window, so that I can offer real options to the customer in the conversation without human handoff.
UI ReferenceN/A — runtime backend behavior

Acceptance Criteria (ATDD / Gherkin)

Scenario: AC-1 — Happy path slot lookup
Type: Happy path

Given OAuth is Connected, a template exists, and access_token is valid
When the agent invokes list_slots(template_id)
Then Qontak calls Google freeBusy with the token + calendar_id + lookup_window
And returns a slot list aligned to the template's duration_minutes
And calendar_slot_lookup event fires with slot_count_returned, status="success", latency_ms
Scenario: AC-2 — Token refresh inherited from S07
Type: Inherited behavior

Given the access_token's TTL has expired (or is near-expired) between calls
When the agent invokes list_slots
Then auto-refresh handled by GCAL-S07 (proactive at 50min TTL; reactive on 401)
And the refresh is transparent to this story (no additional refresh logic implemented here)
Scenario: AC-3 — Date hint biases slot selection
Type: Edge case — date hint

Given the customer says "next Tuesday" and the agent passes optional_date_hint=2026-05-12
When Qontak builds the slot list
Then slots in the lookup window are returned
But the list is biased toward slots near 2026-05-12
Scenario: AC-4 — Empty lookup window
Type: Edge case — no availability

Given the calendar is fully booked across the lookup window
When the agent invokes list_slots
Then an empty slot list is returned
And calendar_slot_lookup fires with slot_count_returned=0
And the agent's prompt logic (upstream) handles the "no slots available" message
Scenario: AC-5 — Token refresh failure cascade
Type: Constraint enforcement — token validity

Given the refresh_token is revoked or invalid
When token refresh fails
Then calendar_token_refresh_failed fires
And calendar_slot_lookup returns status="failed", failure_reason="token_invalid"
And the banner flips to Connection Lost
And the agent prompt receives a failure signal
Scenario: AC-6 — Large date range performance
Type: Volume — performance constraint

Given lookup_window_days=30 and the calendar has 200+ events in range
When the agent invokes list_slots
Then Google freeBusy handles up to 5 calendars / 90-day max per call
And total latency is ≤ 1.5s p95 (per S4 §Performance)
And slot computation paginates if needed

Errors / Unhappy Path

Scenario: ERR-1 — Google 5xx
Given the agent invokes list_slots
When Google returns a 5xx error
Then calendar_slot_lookup fires with status="failed", failure_reason="api_5xx"
And the agent returns a failure signal to runtime prompt
Scenario: ERR-2 — Quota exceeded
Given a high-volume agent
When Google returns 429 quota error
Then Qontak backs off and retries once
And if still 429, calendar_slot_lookup fires with failure_reason="quota_exceeded"
And the banner flips to Quota Exceeded
Scenario: ERR-3 — Bad template_id
Given the agent invokes list_slots with a template_id that doesn't exist
When the template lookup fails (deleted while conversation in progress)
Then failure_reason="template_not_found" is returned
And the agent runtime handles the error path
AspectDetail
Permission — CAN triggerAI Agent runtime only (authenticated via agent's app credential)
Permission — CANNOTAny external caller
UI — Backend storyNo UI of its own
Data — ReturnedSlot list with start_iso, end_iso per slot
FieldTypeRequiredSource
template_idstringYesAgent prompt
optional_date_hintISO dateNoCustomer message NLU
available_slots[]array of {start, end}YesGoogle freeBusy
latency_msintegerYesBackend
statusenum (success/failed)YesBackend
failure_reasonstringIf failedBackend

Story GCAL-S04 — Agent creates a calendar event and sends invite

No designer review needed — backend behavior; agent confirms in chat via runtime prompt logic (out of scope per S3 #11).

FieldValue
PriorityMust Have — this story IS the closed-loop conversion event
Before stateAgent invoked list_slots per S03. Customer picked a slot. No event yet on Google's side.
After deltaAgent invokes create_event(template_id, slot_start, customer_data). Qontak fills template placeholders, POSTs to Google events with sendUpdates=all, receives event_id. Google sends invite. calendar_booking_created + calendar_invite_sent fire.
User storyAs the AI Agent at runtime, I want to create a calendar event with the right title, time, attendees, and Google-sent invite, so that the customer's booking is real and immediately visible in their own calendar.
UI ReferenceN/A — runtime backend behavior

Acceptance Criteria (ATDD / Gherkin)

Scenario: AC-1 — Happy path create event
Type: Happy path — conversion event

Given the customer picked a slot and the agent has customer_data + template_id
When the agent invokes create_event(template_id, slot_start, customer_data)
Then Qontak fills template placeholders with conversation context
And POSTs to Google Calendar events endpoint with sendUpdates=all
And receives event_id from Google
And Google sends invite emails to all attendees
And calendar_booking_created fires with event_id, attendee_count, time_to_book_sec
And calendar_invite_sent fires
And success + event_id is returned to the agent
Scenario: AC-2 — Token refresh inherited from S07
Type: Inherited behavior

Given the access_token expires between list_slots and create_event
When the agent invokes create_event
Then auto-refresh is handled by GCAL-S07 (proactive + reactive + single-flight)
And the refresh is transparent to this story
Scenario: AC-3 — Slot conflict (concurrent booking)
Type: Edge case — concurrency

Given another agent or conversation booked the same slot between list_slots and create_event
When Google processes the create_event POST
Then Google returns event creation success with a conflict warning
And Phase 1 does NOT prevent double-booking on Google's side
(Google's standard behavior is to allow concurrent events)
And the agent treats this as success
Scenario: AC-4 — Performance constraint
Type: Constraint — performance SLA

Given a high-traffic period
When the agent invokes create_event
Then total latency is ≤ 2s p95 from invocation to event_id returned (per S4 §Performance)
Scenario: AC-5 — Quota exceeded
Type: Volume — quota

Given the customer is at Google's API quota limit
When the agent invokes create_event
Then Google returns 429
And Qontak backs off and retries once
And if still 429, calendar_booking_failed fires with failure_reason="quota_exceeded"
And the banner flips to Quota Exceeded

Errors / Unhappy Path

Scenario: ERR-1 — Google 5xx
Given the agent invokes create_event
When Google returns a 5xx error
Then Qontak retries once with backoff
And if still failing, calendar_booking_failed fires with failure_reason="api_5xx"
And the agent's prompt receives a failure signal
Scenario: ERR-2 — Malformed customer email
Given the customer's email in customer_data is malformed
When Google rejects the POST with HTTP 400
Then calendar_booking_failed fires with failure_reason="invalid_attendee_email"
And the agent re-prompts the customer for a valid email
Scenario: ERR-3 — Token invalidated mid-call
Given the agent invokes create_event
When the access_token refresh fails mid-call (revocation)
Then calendar_token_refresh_failed + calendar_booking_failed both fire
And the banner flips to Connection Lost
And the conversation pauses with: "Couldn't confirm your booking right now — a team member will follow up."
And the agent hands off to a human
Scenario: ERR-4 — Event created but invite send fails partially
Given the event creation POST succeeds
When Google returns success but with an attendeesOmittedFromInvite warning
Then event_id is stored
And the banner stays Connected
And calendar_invite_sent fires with invite_status="partial_failure"
And a downstream alert surfaces: "couldn't email customer; please contact them directly"
AspectDetail
Permission — CAN triggerAI Agent runtime only
Permission — CANNOTExternal callers
UI — Backend storyNo UI of its own; agent confirms in chat via runtime prompt logic
Data — Returnedevent_id, start, end, attendees[], html_link
FieldTypeRequiredSource
template_idstringYesAgent prompt
slot_startISO datetimeYesCustomer-selected slot
customer_dataobject (name, email, phone, custom fields)YesCaptured by agent
event_idstringReturnedGoogle response
time_to_book_secintegerYesnow - conversation_start_for_this_booking
attendee_countintegerYesTemplate mapping + customer email

🎨 Story GCAL-S05 — Handle token refresh failure / re-authorization

🎨 Designer review: Yes — frontend story. Wulan/Rizky review the N2 inline OAuth status row state transitions (connected → connection-lost → re-auth → connected) and the Re-authorize UX inside Screen 3 (Action details form). Full MCP Pixel prompt: Wireframes v4.2 → Screen 5.

FieldValue
PriorityShould Have — Closed Beta scale allows manual intervention; full self-serve re-authorize UX matters more at Open Beta and GA scale. Acceptable to defer to Phase 1.5 if Stage 2 capacity is tight.
Before stateOAuth Connected; tokens working. N2 inline OAuth status row inside Screen 3 = connected state.
After deltaWhen token refresh fails (revoked by customer's Google admin, refresh token expired, scope downgraded), N2 row → connection-lost state (MpBanner variant="danger"). [Re-authorize Google Calendar] CTA surfaces inline. Click → re-runs S01 OAuth flow; attached Calendar actions remain intact.
User storyAs an Admin/Supervisor, I want a clear "Connection Lost — re-authorize" path when my Google tokens expire or get revoked, so that I can restore booking capability without losing my attached Calendar actions or AI Agent configuration.
UI ReferenceS11 §Screen 3 — Action details form → N2 inline OAuth status row, connection-lost state (with inline [Re-authorize Google Calendar] CTA). Per S11 §Per-action OAuth status row state machine.

Acceptance Criteria (ATDD / Gherkin)

Scenario: AC-1 — Detect refresh failure
Type: Happy path — failure detection

Given the agent attempts a Calendar API call and token refresh fails
When the backend catches the refresh error
Then calendar_token_refresh_failed fires with failure_reason
And the N2 inline OAuth status row inside Screen 3 flips to connection-lost
And an admin email notification is sent (if configured)
Scenario: AC-2 — Re-authorize from Connection Lost
Type: Happy path — re-authorize

Given the N2 inline OAuth status row inside Screen 3 shows connection-lost
When the admin clicks [Re-authorize Google Calendar]
Then the OAuth flow from GCAL-S01 re-runs
And on success, new tokens are stored
And the N2 inline OAuth status row flips back to connected
And existing attached Calendar actions remain unchanged
Scenario: AC-3 — Re-authorize with different Google account
Type: Edge case — account mismatch

Given the admin re-authorizes but selects a different Google account at Google's account picker
When the new OAuth flow completes
Then attached Calendar actions remain bound to the agent
But the calendars they reference may no longer be accessible
And the N2 inline OAuth status row shows connected
And actions with stale calendar_id will fail at runtime per S03/S04 (graceful failure)
And the admin needs to edit each action's Calendar dropdown field inside Screen 3
Scenario: AC-4 — Failure during active conversation
Type: Edge case — mid-conversation failure

Given an agent invokes list_slots or create_event mid-conversation
And the token refresh fails
When the runtime call returns failure
Then per S03/S04 ERRs:
The agent prompt receives a failure signal
Runtime prompt logic (out of scope) handles UX in chat (human handoff or graceful message)
The N2 inline OAuth status row inside Screen 3 flips to connection-lost in admin UI

Errors / Unhappy Path

Scenario: ERR-1 — Re-authorize flow fails
Given the N2 inline OAuth status row inside Screen 3 shows connection-lost
When the admin clicks [Re-authorize] and the OAuth flow fails (S01 ERR paths)
Then the N2 row stays connection-lost
And the admin sees an error toast
And can retry
Scenario: ERR-2 — Re-authorize to completely different Google Workspace
Given the admin re-authorizes with a different Google Workspace
When the new tokens are stored
Then the calendars referenced by attached actions are no longer accessible
And actions with stale calendar_id fail at runtime per S03/S04
And the admin must update each action's Calendar dropdown field inside Screen 3

Backend implementation cue (BE-canonical, per RFC Google Sheets Workflow Nodes; see also S12.1): The connection-lost state is detected when the refresh-on-401 attempt in GoogleCalendar::CalendarHttpClient fails (invalid_grant, network error, scope revoked, etc. — see GCAL-S07). The Re-authorize flow updates the existing organization_connections row for (org_id, 'google_calendar') in place — does NOT create a new row (single credential per org per provider in v1). N2 row transitions: connected → (refresh fails) → connection-lost → (user clicks Re-authorize) → connected. Does NOT change Gherkin AC scope.

AspectDetail
Permission — CANAdmin, Supervisor
Permission — CANNOTAgent role, Read-only
UI — Connection LostN2 inline OAuth status row inside Screen 3 = connection-lost state (MpBanner variant="danger"): "Connection lost — your Google Calendar authorization is no longer valid. Bookings are paused." + inline [Re-authorize Google Calendar] CTA
UI — Re-authorizingSame loading states as S01
UI — Restored (Connected)N2 row = connected state (success check + user email); attached Calendar actions remain
FieldTypeRequiredSource
agent_idstringYesURL param
failure_reasonenum (revoked / expired / scope_downgrade / unknown)YesBackend
last_known_good_attimestampYesBackend
re_authorized_attimestampWhen successBackend

🎨 Story GCAL-S06 — Cascade delete AI Agent + tokens + templates

🎨 Designer review: Yes — extends the parent AI Agent delete confirmation modal (Screen 5, owned by parent AI Agent PRD) via its cascadeWarnings prop with Calendar-specific warning copy. Wulan/Rizky review the modal layout + warning hierarchy. Full MCP Pixel prompt: Wireframes v4.2 → Screen 5 — parent AI Agent delete modal.

FieldValue
PriorityMust Have — without cascade, deleted agents leave orphaned tokens (security exposure)
Before stateAI Agent has Google Calendar action: OAuth tokens + N attached Calendar actions bound.
After deltaAgent deletion cascade-purges tokens + attached Calendar actions + emits per-action calendar_template_configured action=deleted. Tokens on Google's side are NOT proactively revoked.
User storyAs an Admin/Supervisor, I want deleting an AI Agent to also remove its bound Google OAuth tokens and attached Calendar actions, so that I don't leave orphaned secrets in Qontak storage or stale configuration in the system.
UI ReferenceParent AI Agent delete confirmation modal (Screen 5, owned by parent AI Agent PRD) with Calendar-specific copy injected via the cascadeWarnings prop. This PRD does NOT own the modal — it owns only the Calendar-specific warning string + cascade behavior.

Acceptance Criteria (ATDD / Gherkin)

Scenario: AC-1 — Cascade delete in Connected state
Type: Happy path

Given an AI Agent has OAuth Connected + 1+ attached Calendar actions
When the admin deletes the AI Agent via the parent AI Agent delete modal (Screen 5)
Then the AI Agent record is deleted (per parent PRD)
And bound tokens are purged from encrypted storage
And all attached Calendar actions are purged
And calendar_template_configured fires with action="deleted" for each action
And the N2 inline OAuth status row inside Screen 3 is removed alongside the agent
Scenario: AC-2 — Cascade delete in Connection Lost state
Type: Edge case — already-invalid tokens

Given an AI Agent is in connection-lost status (tokens already invalid) + has attached Calendar actions
When the agent is deleted via the parent AI Agent delete modal (Screen 5)
Then attached Calendar actions are purged
And the refresh_token entry is purged (even though already invalid)
And no token_refresh_failed retries are triggered
Scenario: AC-3 — Role gate
Type: Constraint — role gate (per S4 DELETE)

Given the user's role is NOT Admin or Supervisor
When the user attempts to delete an AI Agent
Then the delete action is not available in the UI
And server-side returns HTTP 403 (existing parent PRD behavior)
Scenario: AC-4 — Bulk deletion
Type: Volume — concurrent deletes

Given a customer has 5 AI Agents with Calendar action attached
When the customer deletes them sequentially
Then each deletion runs independently
And each cascade purges its own tokens + attached Calendar actions
And no shared-state corruption occurs between agents
Scenario: AC-5 — Calendar-specific confirmation modal copy
Type: Happy path — confirmation copy

Given the admin clicks Delete on an agent with attached Calendar action(s)
When the parent AI Agent delete modal (Screen 5) opens
Then the `cascadeWarnings` slot renders Calendar-specific text:
"This will also remove the Google Calendar connection and all attached Calendar actions for this agent.
(Bookings already on Google Calendar will NOT be affected.)"

Errors / Unhappy Path

Scenario: ERR-1 — Partial cascade failure
Given the agent is deleted
When the token purge fails (storage error)
Then an alert is logged for ops
And the orphan token entry is purged by the next nightly cleanup cron (per S4.7)
Scenario: ERR-2 — Customer wants to revoke on Google's side too
Given the customer wants to revoke OAuth at Google
When they ask how
Then the customer playbook documents:
"To fully revoke, remove the app from your Google Workspace admin console at
myaccount.google.com/connections"
And this is out of Qontak's scope
AspectDetail
Permission — CAN deleteAdmin, Supervisor (per S4)
Permission — CANNOTOther roles
UI — Confirmation modalParent AI Agent delete modal (Screen 5) with Calendar-specific warning rendered via cascadeWarnings prop
UI — Post-deleteAgent removed; toast "AI Agent deleted."
FieldTypeRequiredSource
agent_idstringYesURL param
had_oauth_connectedbooleanYesDerived
attached_actions_purged_countintegerYesDerived

Story GCAL-S07 — Auto-refresh OAuth access token (proactive + reactive, concurrent-safe)

No designer review needed — backend behavior; failure cascades to the N2 inline OAuth status row inside Screen 3 (covered by GCAL-S05 review).

FieldValue
PriorityMust Have — without proactive auto-refresh, OAuth access tokens expire every 60 minutes and cause silent booking failures. Daily-use conversational feature requires baseline auto-refresh.
Before statePhase 1 originally treated token refresh as edge-case ACs. No dedicated mechanism; no proactive refresh; no concurrent-refresh safety.
After deltaDedicated background mechanism. Proactive: refresh at ≤10min remaining on 60min TTL. Reactive: on 401 from Google, refresh + retry once. Concurrent-safe: single-flight per agent_id. Atomic: token replacement is atomic. On failure: cascade to GCAL-S05.
User storyAs the Qontak Chatbot system, I want to proactively and reactively refresh Google OAuth access tokens with concurrency safety, so that all active customer conversations can invoke calendar actions seamlessly regardless of the 60-minute TTL.
UI ReferenceN/A — backend behavior; status reflected via the N2 inline OAuth status row inside Screen 3

Acceptance Criteria (ATDD / Gherkin)

Scenario: AC-1 — Proactive refresh at 50min TTL
Type: Happy path — proactive

Given an agent's access_token has ≤10 minutes remaining on its 60min TTL
When the background refresh job runs (every 1min cron or equivalent scheduler)
Then Qontak POSTs to Google's token endpoint with the refresh_token
And a new access_token is returned
And the new token is stored atomically
And calendar_token_refreshed event fires with refresh_type="proactive" and latency_ms
And no user-facing booking is disrupted
Scenario: AC-2 — Reactive refresh on 401 mid-call
Type: Happy path — reactive

Given the agent is mid-call to Google Calendar API (Behavior 4 or 5)
When Google returns HTTP 401 (token unexpectedly expired)
Then Qontak triggers a refresh inline
And the new access_token is stored
And the original API call is retried once with the new token
And success is returned to the agent
And calendar_token_refreshed fires with refresh_type="reactive" and latency_ms
And total user-facing latency added is ≤ 500ms p95 (per S4 §Performance)
Scenario: AC-3 — Single-flight concurrency
Type: Concurrency — single-flight

Given 5 active conversations all invoke list_slots or create_event simultaneously
And all detect access_token with <10min TTL
When each invocation attempts to refresh
Then a mutex (or distributed lock) per agent_id ensures exactly ONE refresh fires to Google
And the other 4 conversations wait ≤ 500ms
And the 4 waiting conversations reuse the newly-stored access_token
And only ONE calendar_token_refreshed event fires
Scenario: AC-4 — Atomic token replacement
Type: Atomicity — no torn writes

Given a refresh completes and storage replace is in progress
When a concurrent reader requests the access_token at that exact moment
Then the reader gets either the OLD or the NEW value, never a torn/partial value
And no call ever uses a partial or corrupted token
Scenario: AC-5 — Refresh latency alert
Type: Constraint enforcement — performance

Given a refresh fires (proactive or reactive)
When the refresh roundtrip to Google takes > 500ms p95
Then a latency alert fires (per S6 alerts table)
And ops investigates Google's response time / network
Scenario: AC-6 — Failure cascade to GCAL-S05
Type: Failure cascade

Given a refresh is attempted (revoked, expired, network failure)
When Google returns 400/401/5xx on the refresh call
Then calendar_token_refresh_failed fires with failure_reason
And the N2 inline OAuth status row inside Screen 3 flips connected → connection-lost (per GCAL-S05)
And any in-flight booking fails gracefully with the agent message:
"Couldn't confirm your booking right now — a team member will follow up."
And the admin sees the inline [Re-authorize Google Calendar] CTA in the N2 row
Scenario: AC-7 — Idempotency between proactive and reactive paths
Type: Idempotency

Given a reactive refresh is in progress
When the proactive background job also fires for the same agent_id
Then the single-flight mutex catches both
And only ONE Google call fires
And both code paths reuse the same new token
And no duplicate calendar_token_refreshed events fire

Errors / Unhappy Path

Scenario: ERR-1 — Background job crashes mid-refresh
Given a refresh is in progress
When the background job process crashes
Then the mutex auto-releases after its TTL (e.g., 10s)
And the next call retries
And no zombie locks remain
Scenario: ERR-2 — Storage error on atomic replace
Given a refresh response is received
When the encrypt + store step fails
Then a log alert fires
And the old token remains valid until its natural TTL expiry (no data loss)
And the next refresh attempt retries
And if persistent, cascade to S05
Scenario: ERR-3 — Google token endpoint quota
Given a high-volume customer with many concurrent refreshes
When Google's token endpoint quota is hit (typically per-OAuth-client, high limit)
Then Qontak backs off and retries
And if persistent, an ops alert fires

Backend implementation cue (BE-canonical, per RFC Google Sheets Workflow Nodes; see also S6.6 + S12.1): Refresh is owned by Repositories::NodeResources::GoogleCalendar::CalendarHttpClient, which wraps every Google Calendar API call. The implementation mirrors MekariQontakCrm::CrmHttpClient#request_with_auth (copy-paste with Google-specific token endpoint). Refresh-on-401 behavior:

  1. On any 401 response from Google, attempt refresh via POST https://oauth2.googleapis.com/token using the stored refresh_token from organization_connections.auth_data for (org_id, 'google_calendar').
  2. On refresh success: update organization_connections.auth_data with the new access_token + new expires_at. Retry the original request once.
  3. On refresh failure: mark credential as connection-lost (handled by GCAL-S05). Trigger the N2 inline OAuth status row state change in Screen 3.

The proactive vs reactive vs single-flight vs atomic-replacement ACs above (AC-1 through AC-7) stand as-is — they describe the high-level semantics; this cue declares the canonical file + token endpoint + storage column the implementation uses. Does NOT change Gherkin AC scope.

AspectDetail
Permission — CAN triggerSystem (background job) + Agent runtime (reactive); no human trigger
Permission — CANNOTExternal callers; admin UI does not directly trigger refresh
UI — Backend storyNo UI of its own; failure cascades to N2 inline OAuth status row inside Screen 3 via S05
Data — refresh metadatalast_refresh_at, next_refresh_at, refresh_count_24h
FieldTypeRequiredSource
agent_idstringYesInternal scheduler / reactive trigger
refresh_tokenstring (sensitive)YesEncrypted storage
access_token (new)string (sensitive)Yes (output)Google response
refresh_typeenum (proactive / reactive)YesBackend
latency_msintegerYesBackend
next_refresh_attimestampYesnew_expiry - 10min

Story GCAL-S08 — Backend: node_registries seed + executor + HTTP client + lookup resources

No designer review needed — backend infrastructure; no UI of its own. Mirrors the canonical mekari_qontak_crm pattern per RFC Google Sheets Workflow Nodes. See also S6.6 (file inventory) and S12.1 (credential storage).

FieldValue
PriorityMust Have — the FE-facing stories (S01, S02, S03, S04, S05, S07) all depend on these BE files existing.
Before stateNo google_calendar provider module exists in the codebase. node_type_registry.rb has no google_calendar entry. PROVIDER_MODULES constant in lookup_resources.rb does not include GoogleCalendar. No db/seeds/google_calendar_nodes.rb.
After deltaAll canonical files exist (executor, HTTP client, lookup resources, seed) and the two registry files are updated. Seed produces exactly one node_registries row for the Calendar booking action and is idempotent (re-running does not duplicate). Specs cover executor happy path, lookup happy path, refresh-on-401, seed idempotency.
User storyAs the Qontak Chatbot backend, I want a google_calendar provider module that mirrors the proven mekari_qontak_crm pattern, so that the FE-facing OAuth, lookup, and booking stories have a working backend to call into.
UI ReferenceN/A — backend infrastructure

Acceptance Criteria (ATDD / Gherkin)

Scenario: AC-1 — Executor file exists and mirrors mekari_qontak_crm
Type: Happy path — file existence

Given the BE team implements GCAL-S08
When the codebase is inspected
Then app/core/repositories/node_executions/nodes/google_calendar/execute.rb exists
And it defines Repositories::NodeExecutions::Nodes::GoogleCalendar::Execute
And the structure mirrors Repositories::NodeExecutions::Nodes::MekariQontakCrm::Execute
And it uses process_path for path interpolation
Scenario: AC-2 — HTTP client implements refresh-on-401 via Google's token endpoint
Type: Happy path — refresh-on-401

Given app/core/repositories/node_resources/google_calendar/calendar_http_client.rb exists
And it defines Repositories::NodeResources::GoogleCalendar::CalendarHttpClient
When the client receives a 401 from a Google Calendar API call
Then it POSTs to https://oauth2.googleapis.com/token with grant_type=refresh_token
and the stored refresh_token from organization_connections.auth_data for (org_id, 'google_calendar')
And on success, updates organization_connections.auth_data with new access_token + expires_at
And retries the original request exactly once
And on refresh failure, marks the credential as connection-lost (cascades to GCAL-S05)
Scenario: AC-3 — Lookup module exposes resource_key='calendar'
Type: Happy path — lookup contract

Given app/core/repositories/node_resources/google_calendar/lookup_resources.rb exists
And it defines Repositories::NodeResources::GoogleCalendar::LookupResources
When the FE invokes lookup with resource_key='calendar'
Then the module calls GET https://www.googleapis.com/calendar/v3/users/me/calendarList?fields=items(id,summary,primary)
And returns each item as {name: summary, value: id}
And marks the primary calendar with a badge / sorts it to the top
Scenario: AC-4 — node_type_registry registers google_calendar executor
Type: Constraint — registry wiring

Given app/core/repositories/node_executions/node_type_registry.rb is updated
When the registry is inspected
Then it contains the mapping 'google_calendar' => 'GoogleCalendar::Execute'
Scenario: AC-5 — PROVIDER_MODULES registers GoogleCalendar
Type: Constraint — registry wiring

Given app/api/frontend_service/v1/node_resources/use_cases/lookup_resources.rb is updated
When the PROVIDER_MODULES constant is inspected
Then it includes GoogleCalendar (alongside MekariQontakCrm and any other native providers)
Scenario: AC-6 — Seed creates exactly one node_registries row and is idempotent
Type: Idempotency — re-runnable seed

Given db/seeds/google_calendar_nodes.rb exists
When `rails db:seed:google_calendar_nodes` (or equivalent) is run on a fresh DB
Then exactly one row is created in node_registries for the Google Calendar booking action
And the row's properties JSON follows the canonical RFC §6 shape, with the 8 fields:
calendar_id (select, is_rl=true, resource_types=[{provider:'google_calendar', resource_key:'calendar'}], required, destination=body)
duration_minutes (input/number, required, validation min=5/max=480, destination=body)
lookup_window_days (slider, required, validation min=1/max=30/step=1, destination=body)
event_title_pattern (input/text, required, destination=body, placeholders documented)
location (input, optional, destination=body)
event_description (textarea, optional, destination=body)
attendees_customer_email_field (select, required, destination=body, options from conversation context)
attendees_always_cc (input, optional, destination=body)
When the seed is re-run
Then no duplicate row is created and the existing row is updated in place (upsert semantics)
Scenario: AC-7 — Spec coverage
Type: Quality — spec coverage

Given the spec/ tree is inspected
Then specs exist covering:
executor happy path (mirrors mekari_qontak_crm executor specs)
lookup happy path (calendarList API → {name, value} mapping)
refresh-on-401 (401 → token refresh → retry → success)
seed idempotency (re-run does not duplicate)

Errors / Unhappy Path

Scenario: ERR-1 — Seed runs against a DB where the row exists but is malformed
Given a node_registries row for google_calendar exists but is missing required properties keys
When the seed re-runs
Then the seed upserts the canonical properties JSON, overwriting the malformed values
And an info log records the upsert (not an error)
Scenario: ERR-2 — Refresh-on-401 fails with invalid_grant
Given the HTTP client receives 401 and the refresh attempt returns invalid_grant
When the failure propagates
Then the credential is marked connection-lost in organization_connections
And the cascade to GCAL-S05 fires (N2 inline OAuth status row → connection-lost state)
AspectDetail
Permission — CANBE team (code merge); no runtime user trigger
Permission — CANNOTExternal callers
UI — Backend storyNo UI of its own; powers S01, S02, S03, S04, S05, S07
Files — Newapp/core/repositories/node_executions/nodes/google_calendar/execute.rb, app/core/repositories/node_resources/google_calendar/calendar_http_client.rb, app/core/repositories/node_resources/google_calendar/lookup_resources.rb, db/seeds/google_calendar_nodes.rb, specs
Files — Modifiedapp/core/repositories/node_executions/node_type_registry.rb, app/api/frontend_service/v1/node_resources/use_cases/lookup_resources.rb
Deliverable — follow-up RFCExact node_registries.properties JSON for the Calendar booking action — owned by Eko + PM, mirroring RFC §6 Sheets format.

Story GCAL-S09 — Feature flag rollout: FEATURE_GOOGLE_CALENDAR_NODES

No designer review needed — rollout control; no UI of its own. Mirrors the canonical rollout pattern per RFC Google Sheets Workflow Nodes.

FieldValue
PriorityMust Have — the entire Phase 1 ships behind this flag; no way to control rollout otherwise.
Before stateNo FEATURE_GOOGLE_CALENDAR_NODES flag exists. Native integrations row would be visible to all orgs as soon as the seed merges.
After deltaFlag exists, default OFF. When OFF: the Google Calendar row is hidden from the Action drawer (Other integration group) and the OAuth handshake endpoint returns 404. When ON per-organization: full Phase 1 flow available for that org only. Closed Beta enabling tracked per ANCHOR S8a (2026-05-06) — Citroen + 2-3 friendly customers.
User storyAs the Qontak Chatbot rollout owner, I want a per-organization feature flag controlling Google Calendar visibility and reachability, so that Closed Beta can validate with a small cohort before global enable.
UI ReferenceN/A — backend rollout control

Acceptance Criteria (ATDD / Gherkin)

Scenario: AC-1 — Flag exists, default OFF
Type: Constraint — flag wiring

Given the BE team implements GCAL-S09
When the feature flag service is inspected
Then FEATURE_GOOGLE_CALENDAR_NODES is registered
And its default value is OFF
And it is scopable per organization
Scenario: AC-2 — When OFF, Native integrations row is hidden
Type: Happy path — flag OFF

Given FEATURE_GOOGLE_CALENDAR_NODES = OFF for org X
When an Admin/Supervisor in org X opens the Action drawer
Then the `Google Calendar` row is NOT rendered in the `Other integration` group
And the OAuth handshake endpoint returns HTTP 404 (not 403, not 401)
And no `Google Calendar`-related UI surfaces are reachable for org X
Scenario: AC-3 — When ON per-organization, full Phase 1 flow is available
Type: Happy path — flag ON

Given FEATURE_GOOGLE_CALENDAR_NODES = ON for org Y
When an Admin/Supervisor in org Y opens the Action drawer
Then the `Google Calendar` row IS rendered in the `Other integration` group (per GCAL-S01 AC-1)
And the OAuth handshake endpoint is reachable (per GCAL-S01)
And the per-action details form (Screen 3) opens normally
And all P1 stories (S01, S02, S03, S04, S05, S07) function end-to-end for org Y
Scenario: AC-4 — Closed Beta enables per-organization for Citroen + 2-3 friendlies
Type: Rollout — Closed Beta

Given Closed Beta begins (per S5 Rollout table — late May / June 2026)
When the rollout owner (PM + CSM) enables FEATURE_GOOGLE_CALENDAR_NODES per org
Then it is enabled for Citroen + 2-3 friendly customers (named in S5 / ANCHOR S8a)
And not enabled for any other org
And monitoring (per S6 Observability + S6.5) tracks latency, refresh failures, quota for the enabled cohort only
Scenario: AC-5 — Global enable gated on one sprint of monitoring
Type: Rollout — Open Beta / GA gate

Given Closed Beta has run for 4 weeks
And the S6c Open Beta success gate is met
And one sprint (2 weeks) of monitoring shows latency, refresh failures, and quota within S6b thresholds
When the rollout owner enables FEATURE_GOOGLE_CALENDAR_NODES globally
Then all eligible orgs (Native Integrations add-on purchasers, per S4) see the `Google Calendar` row

Errors / Unhappy Path

Scenario: ERR-1 — Flag service unavailable mid-request
Given the FE requests the Action drawer
When the feature flag service is unreachable
Then the system fails CLOSED (treats flag as OFF) — the `Google Calendar` row is hidden
And an ops alert fires
And no 5xx is surfaced to the user
Scenario: ERR-2 — Org transitions from ON to OFF mid-session
Given an Admin was using the per-action details form with the flag ON
When the rollout owner toggles the flag OFF for that org
Then any in-flight save attempt returns 404 (consistent with AC-2)
And the Admin sees a non-destructive message: "Google Calendar is not available for your account."
And existing organization_connections.google_calendar rows remain in the DB (no cascade purge from flag toggle)
AspectDetail
Permission — CAN toggleRollout owner (PM + Platform Eng), via the feature flag service admin UI
Permission — CANNOTEnd users (Admin, Supervisor, etc.) — flag is opaque to UI
UI — Backend storyNo UI of its own; governs visibility of all S01–S08 surfaces
Threshold targetsSee S6b Success Metrics for monitoring thresholds gating global enable

Negative Scenarios (mapped to S3 Non-Goals)

Scenario: NEG-1 — No other Google products in P1 (S3 #1)
Given the admin opens the Action drawer
When they search/scroll for Sheets / Drive / Gmail in the `Other integration` group
Then no Sheets / Drive / Gmail rows are present in P1
And the only Phase 1 native row in `Other integration` is `Google Calendar`
Scenario: NEG-2 — No other calendar providers in P1 (S3 #2)
Given the admin searches for Outlook / Apple Calendar in the Action drawer
When they look at the `Other integration` group
Then no Outlook / Apple rows are visible in P1
Scenario: NEG-3 — Single shared calendar only in P1 (S3 #3)
Given the admin tries to assign different reps to different bookings
When they open the Action details form (Screen 3, extends E7)
Then no per-rep / per-team controls are shown
And the admin sees one Calendar dropdown field per attached Calendar action only
Scenario: NEG-4 — No reschedule/cancel by agent (S3 #4)
Given the customer asks the agent to reschedule an existing booking
When the agent processes the request at runtime
Then the agent cannot invoke a reschedule action (it does not exist in P1)
And the prompt logic (out of scope) hands off to a human
Scenario: NEG-5 — No two-way sync (S3 #5)
Given the rep changes an event time directly in Google Calendar
When the change occurs on Google's side
Then Qontak's view of the booking remains stale (no automatic update)
And if the customer asks the agent about the event, the agent has no knowledge of the change
And this is out of P1 scope
Scenario: NEG-6 — Single events only (S3 #6)
Given the customer asks "book me every Tuesday for the next 4 weeks"
When the agent processes the request
Then the agent cannot invoke recurring (can only create one event per create_event call)
And the prompt logic handles "I can only book one at a time"
Scenario: NEG-7 — No group bookings (S3 #7)
Given the customer asks "book a session for me and 3 colleagues"
When the agent processes the request
Then the agent creates an event with a single customer attendee
And if the customer asks to add others, the prompt logic hands off
Scenario: NEG-8 — No calendar widget (S3 #8)
Given the customer wants to see a visual calendar
When the agent presents booking options
Then the agent presents slot options as text/buttons in chat only
And no embedded calendar UI is rendered
Scenario: NEG-9 — No WhatsApp/SMS reminders from Qontak (S3 #9)
Given the customer expects a WhatsApp reminder 1hr before
When the booking is created
Then Google sends standard email reminder (per Google Calendar settings)
And no Qontak-side WhatsApp/SMS reminder fires
Scenario: NEG-10 — Data residency (S3 #10)
Given the customer asks if their data stays in Indonesia
When they raise this in a sales conversation
Then the honest answer is:
Customer's calendar data is in Google's regions, governed by their own Google
Workspace data residency policy. Not a Qontak guarantee.
Scenario: NEG-11 — No LLM scope (S3 #11)
Given an AI coding agent consumes this PRD
When it generates acceptance criteria
Then the agent does NOT generate ACs that validate model behavior
(prompt selection, tool selection, fallback strategy)
And those concerns belong in the parent AI Agent runtime PRD

PRD CHANGELOG

VersionDateBySectionTypeSummary
1.02026-05-06ClaudeAllCREATEDInitial NEW PRD. ANCHOR (Part A) created alongside.
1.12026-05-07ClaudeS7, S11, S13MODIFIEDPolish: Component Tree diagram fixed; Dependency Graph in S7; GCAL-S05 → Should Have; explicit Flow Name + Type. Score 9.5 → 9.7.
1.22026-05-07ClaudeHeader, S4, S6, S8, S9, S11, S12, S13MODIFIEDAdded GCAL-S07 (auto-refresh OAuth) as Must Have; AI SDLC + MCP Pixel pattern. MoSCoW: 6 Must · 1 Should.
1.32026-05-12ClaudeS13MODIFIEDExpanded all 7 stories with ATDD content (table format).
1.42026-05-15ClaudeS13REFORMATTEDRewrote all AC / Errors / Negatives as fenced Gherkin scenarios.
1.52026-05-15ClaudeNew 🎨 Designer Review Notes section; inline 🎨 markers on UI-bearing stories (S01, S02, S05, S06); explicit "no designer review needed" notes on backend stories (S03, S04, S07); 🎨 marker added to S11 heading and S6c Internal Alpha owner row.MODIFIEDAdded explicit Designer Review Notes section after blockquote header, identifying which sections Wulan + Rizky need to review (S11 + UI-bearing stories) vs which are backend-only. Includes review gate definition and fallback path (manual UI build in Phase 1.5 if MCP Pixel quality fails).
1.62026-05-15ClaudeS11, top calloutREALIGNEDReplaced MpTabs + NativeIntegrationsTabPanel with the existing Figma drawer pattern: Google Calendar = single row in a new "Native integrations" product group. Added E1–E8 reuse table + N1–N8 new-components table. Cross-linked Wireframes v3.0.
1.72026-05-19ClaudeS11, top calloutREWRITTENMajor realignment after Gap Analysis of v3.0 implementation drift. IA collapsed: no separate Google Calendar config surface; per-action templates per PM decision (a); calendar fields embedded in existing E7 Action details form schema. Dropped 7 v1.5 components as inventions. Added V1–V7 outstanding verifications. Component count 11 → 8.
1.82026-05-22ClaudeHeader, S11, S13 (Flow + GCAL-S01 + NEG-1/NEG-2)REALIGNEDS11 IA correction to Wireframes v4.1 (Figma 2026-05-22). Cancels v1.6/v1.7 invention of a new "Native integrations" sibling group: native rows live under the existing Other integration group of the action drawer (5 groups, fixed order: Mekari Qontak / Talenta / Jurnal / Desty / Other integration). Phase 1 adds exactly one row: Google Calendar. Replaced drawer states list with the 4 canonical P0 states from Figma (Collapse/expand, Scrolled, Search found, Search not found) and moved Loading + Failed-to-load to P1 under new V8 verification. Screen 3 (per-action details form) clarified as Pixel MCP-only spec — no Figma node required (V2 dropped for Screen 3). Added hard brand-asset guard: every brand SVG slot must be marked [BRAND_SVG_PENDING_V4 — DO NOT CODE-GEN WITHOUT REPLACING]; code-gen forbidden until V4 closes; no MpIcon name="calendar" placeholder names. V1 marked ✅ RESOLVED 2026-05-22 (single-column right-side drawer). Added V8 (runtime drawer states). S13 Flow steps 3/4/8/9 and GCAL-S01 (After delta, UI Reference, AC-1/AC-2/AC-3/AC-5/AC-6, Permission Unauthorized row) and NEG-1/NEG-2 retargeted from "Native Integrations tab" + GoogleCalendarConfigSection + OAuthConnectionCard + CalendarStatusBanner to "Google Calendar row in Other integration group" + per-action details form (Screen 3, Pixel MCP-only) + inline OAuth status row. Cross-links updated to Native Integrations ANCHOR v1.2 and Mekari Action ANCHOR v1.3.
1.92026-05-22ClaudeHeader, S11 V3 verification gateMODIFIEDMCP replacement. Canonical design-system MCP is now https://ai.mekari.design/mcp (replaces the deprecated pixel-mcp.netlify.app). Updated V3 verification gate in S11. No IA or scope changes.
1.102026-05-22ClaudeTop Designer Review Notes, S11 cross-links, GCAL-S02, GCAL-S03, GCAL-S05, GCAL-S06, GCAL-S07, NEG-3REALIGNEDStory-body cleanup. Realigned GCAL-S02, S05, S06, S07 + top Designer Review Notes to v4.2 IA: replaced references to dropped components (TemplateEditorModal, TemplateList, CalendarStatusBanner, OAuthConnectionCard, CalendarPicker, GoogleCalendarConfigSection, Calendar-variant DeleteAgentModal) with the corresponding v4.2 surfaces (Screen 3 Action details form / N2 inline OAuth status row / Calendar dropdown field / parent AI Agent delete modal). GCAL-S02 retargeted from "template list/editor" surfaces to "one booking template = one attached action row" model: Action details form (Screen 3, extends E7) is the editor; attached actions surface as rows in the Actions tab populated state (Screen 1, extends E3). GCAL-S05 retargeted CalendarStatusBanner state machine to N2 inline OAuth status row state machine inside Screen 3. GCAL-S06 retargeted Calendar-variant delete modal to parent AI Agent delete modal (Screen 5) with cascadeWarnings prop. GCAL-S07 and S03 backend-cascade notes updated to reference the N2 inline OAuth status row. NEG-3 retargeted from CalendarPicker per-template to Calendar dropdown field per attached action. Applied [BRAND_SVG_PENDING_V4] guard sweep — no Mekari product MpIcon placeholders found inside MCP Pixel prompt code blocks (only inside §Brand asset guard / Hard NOs as explicit DO-NOT-USE examples, preserved). Updated stale Wireframes cross-links from v4.1/rev 5 to v4.2/rev 6 throughout S11 (8 refs). No IA, scope, AC count, or constraint changes — only realigning the prose to the canonical Wireframes v4.2.
1.112026-05-22ClaudeHeader (PRD Version, RFC Link), S6 (new S6.6 BE Architecture), S12 (new S12.1 Credential Storage & OAuth), S8a (new decision row), S9 (3 new open questions #13/#14/#15), GCAL-S01/S02/S05/S07 (backend implementation cues), GCAL-S08 + GCAL-S09 (new backend stories), Story Index, ChangelogMODIFIEDRFC alignment (full BE). Encoded BE-canonical auth + architecture per RFC Google Sheets Workflow Nodes into S6 (new S6.6 BE Architecture subsection — file inventory mirroring mekari_qontak_crm), S12 (new S12.1 Credential Storage & OAuth subsection — organization_connections table, https://oauth2.googleapis.com/token, refresh-on-401, quota note), S8a (new decision row dated 2026-05-22 — BE infrastructure mirrors mekari_qontak_crm; credential canonical = organization_connections; single credential per org per provider; credential resolved by HTTP client using org_id + provider code, NOT a user-facing property), GCAL-S01 (backend cue — organization_connections row write/upsert), GCAL-S02 (backend cue — LookupResources pattern with resource_key='calendar'), GCAL-S05 (backend cue — connection-lost via refresh-on-401), GCAL-S07 (backend cue — canonical HTTP client GoogleCalendar::CalendarHttpClient + token endpoint + storage column). Added GCAL-S08 (BE infra: executor + HTTP client + lookup resources + node_registries seed; 7 ACs + 2 ERRs + file inventory) and GCAL-S09 (FEATURE_GOOGLE_CALENDAR_NODES rollout: default OFF, per-org enable, Closed Beta → global; 5 ACs + 2 ERRs). Flagged 3 new open questions in S9 (#13 design-system HTML elements, #14 userinfo.email scope, #15 "per org per provider" vs ANCHOR's "per AI Agent" credential framing tension — needs follow-up alignment). No UX label changes; no Gherkin AC scope changes on existing stories (only ADDED backend-cue blockquotes below the existing ACs); MoSCoW priorities unchanged; S11 untouched; Wireframes v4.2 cross-links preserved. Story count 7 → 9 (all new stories are backend Must Have).