Qontak | Chatbot | AI Agent — Qontak CRM Actions — Adj: Custom fields on create ticket / create deal
ADJUSTMENT to Qontak CRM Actions (as-built). Extends the already-shipped
create deal(QACT-S01) andcreate ticket(QACT-S02) actions so the builder can map the tenant's CRM custom fields — org-level (shared by every pipeline) — for the AI Agent to fill. Everything else about those two actions (auth,{{customer_name}}interpolation, 401 refresh/retry, the fixed standard fields) stays exactly as shipped. Design grounded in theqontak-designerprototype — wireframes committed in-repo atdocumentations/crm-actions-custom-fields-wireframes.md; feasibility grounded inchatbotBE +chatbot-feFE (2026-07-13).v1.6 — as-built reconciliation (2026-07-27). v1.5 recorded GA (frontmatter, header, Rollout) but left the contract itself untouched; the completion-alignment audit then gated the initiative ⛔ NOT-ALIGNED — 6 of 24 composite ACs delivered, 0 test cases in either the repo suite or Pokayoke. v1.6 answers that audit's open questions 1–4 in this contract. It is aligned to the shipped implementation and to BE TECH-RFC Decision 3.7. Two things changed materially since v1.4: custom fields turned out to be org-level (
team_id-scoped), not pipeline-scoped — the pipeline gate (old D-1/D-2) is gone (ratified, closes RFC A-4) — and three drafted capabilities did not ship: array-typed fields, theai_agent_action_custom_fieldsflag, and the custom-field telemetry. They are recorded honestly as deferrals in Section 15 (D-11 → D-15) with follow-up items in Section 16, not rewritten as if never intended. Naming: the product term here is custom field; the CRM API, the BE code, and the FE code all call the same thing an additional field (additional_field_deal/additional_fields[]) — they are the same object.
HEADER BLOCK
| Field | Value |
|---|---|
| PM | Dimas Fauzi Hidayat |
| PRD Version | 1.7 |
| Status | SHIPPED — GA to all clients/users 2026-07-27, partial against this contract. The scalar path is live (BE 1.324.0, FE 3.8.3); array types, the feature flag and the telemetry events did not ship (D-11 → D-13), and the merge reaches the update actions (D-15). Audit: completion-alignment check — ⛔ NOT-ALIGNED, 6 of 24 ACs delivered |
| PRD Type | ADJUSTMENT |
| Epic | BOT-4662 — 26Q3 Chatbot - CRM Actions: Custom Fields (child Epic; initiative umbrella is BOT-4597). |
| Squad | BOT — Chatbot & AI Squad |
| RFC Link | BE TECH-RFC — CRM actions custom fields (+ FE RFC) — merged; Decision 3.7 is the org-level scoping authority |
| Figma Master | Pending — production Figma frames TBD. Interim design reference: Wireframes (in-repo) + qontak-designer prototype |
| Anchor | Yes — Qontak Action — ANCHOR |
| Parent PRD | Qontak CRM Actions — Released Actions (as-built) |
| Labels | epic:qontak-chatbot | module:ai-agent | feature:qontak-action |
| Last Updated | 2026-07-27 |
Status values:
DRAFT→READY→BUILD→SHIPPED.
Table of Contents
- HEADER BLOCK
- 2. CONDITIONAL BLOCK: ADJUSTMENT CONTEXT
- 3. One-liner + Problem
- 4. Target Users + Persona Context
- 5. Non-Goals
- Scope Changes
- 6. Constraints
- 7. Feature Changes
- 8. API & Webhook Behavior
- 9. System Flow + User Stories + ACs
- 10. Rollout
- 11. Observability
- 12. Success Metrics
- 13. Launch Plan & Stage Gates
- 14. Dependencies
- 15. Key Decisions + Alternatives Rejected
- 16. Open Questions
- PRD CHANGELOG
2. CONDITIONAL BLOCK: ADJUSTMENT CONTEXT
| Field | Value |
|---|---|
| CB-A.1 — Parent Anchor | Qontak Action — ANCHOR. Parent PRD: Qontak CRM Actions — Released Actions (as-built), child Epic BOT-4662. |
| CB-A.2 — Adjustment scope (as-built) | The create deal and create ticket action-config surfaces gain an org-level custom-field picker — a dedicated Add field popover listing the org's CRM additional fields (searchable), shipped as AdditionalFieldSelector.vue. The list is not filtered by pipeline: CRM defines additional fields per team_id, so every pipeline in the org shares one list (D-1). Each added field, like the existing fixed fields, carries a Let AI decide / Set manually mode. The BE executor merges the configured values into the CRM write body as an additional_fields[] array (D-14). Config surface is create deal + create ticket; note the BE merge itself is wired for all four write actions incl. update deal / update ticket (D-15). Array-typed fields (Checklist / Multi-select / Tags) are NOT in the shipped scope — see D-11. |
| CB-A.3 — Parent scope preserved (unchanged) | Unchanged: company-token (Bearer) auth + 401 refresh/retry (node_resources/mekari_qontak_crm/crm_http_client.rb:45-77 (401 refresh/retry + Bearer)); {{customer_name}} interpolation; customer_contact_association lead lookup → crm_lead_ids (execute.rb:262-276 (enrich_body_for_deal_create)); the existing fixed fields (Pipeline, Stage, name, creator/owner, customer_contact_association, deal tags); all other actions (update ticket, update deal, assign, resolve, tag, attachment); the ai_agent_action gating and per-action billing components (Section 6 of the parent PRD). This adjustment adds only the custom-field layer on top of the two create actions. |
| CB-A.4 — Reason for adjustment (why now) | Unblocks a live configuration gap. Tenants configuring create deal / create ticket today can only map the hard-coded standard properties — the FE picker offers no path to their org's custom fields (chatbot-fe/modules/ai-agent/constants/ai-agent-actions-constants.ts:75-432) and the BE builds the CRM body only from node-registry properties matched by destination, with no custom-field merge (chatbot/app/core/repositories/node_executions/nodes/mekari_qontak_crm/execute.rb:186-208). Result: every deal/ticket the agent creates lands with the org's custom fields empty, so CS ops must hand-back-fill each record in CRM — and pipelines with required custom fields cannot be automated at all. This is the top blocker for adopting the create actions among orgs whose CRM depends on custom fields. |
3. One-liner + Problem
One-liner: Let the Qontak AI Agent fill a tenant's CRM custom fields — the org's own additional-field definitions, shared across pipelines — when it creates a deal or ticket, so automated records land complete instead of half-empty.
Problem:
The shipped create deal / create ticket actions only let the builder map Qontak's built-in standard properties. Every org that has customized its CRM — the common case for Sales/Service pipelines — configures the action, watches the agent create records, and then finds its own custom fields (ticket type, affected modules, competitor products, region tags, SLA tier, …) blank on every record. CS ops back-fills by hand, defeating the automation; and an org with a required custom field can't automate the create at all because CRM rejects it. The capability gap is purely in the config surface + payload merge — the array primitives the agent needs already exist in the codebase (see Section 14).
Evidence grounding. No Product Intelligence Hub research doc is cited (
sources: []) — this adjustment is driven by a code-grounded configuration gap and theqontak-designerdesign prototype, not by a user-research study. The problem is evidenced by the FE/BE code paths above and confirmed live in the prototype.
4. Target Users + Persona Context
| Persona | Role | Goal | Pain | Workaround |
|---|---|---|---|---|
| Primary — CS Ops Lead / Bot Manager | Customer-side bot owner who configures the AI Agent's create deal / create ticket actions | Have the agent create CRM records that are complete — populated with the org's own custom fields, not just the standard ones | The Add-field picker offers only standard properties; the agent can't touch custom fields, so records are created incomplete | Manually edit every agent-created deal/ticket in Qontak CRM to fill custom fields; or avoid the action for pipelines that require custom fields |
| Secondary — CRM Admin | Owns the org's custom-field (additional-field) definitions in Qontak CRM (settings → customization) — org-level, shared by every pipeline | Trust that fields they define for the org are honoured by automation | Custom fields they define are invisible to the AI Agent config | Tell CS ops which fields to back-fill by hand |
| Runtime actor — AI Agent (fills the values) | The function-calling agent during a live conversation | Fill each mapped custom field from the conversation, choosing valid values (one or more, for array fields) | — | — |
(Full persona background + auth/gating: parent PRD Sections 4 & 6, and the ANCHOR.)
5. Non-Goals
update deal/update ticketconfig surface — this adjustment's builder-facing scope is the two create actions. (As-built caveat: the BE merge is wired for all four write actions — whether the picker is exposed on the update actions depends only on the per-actionnode_registriesseed, not on code. See D-15 and OQ A-6.)- Creating or editing custom-field definitions — the picker reads the org's existing custom fields; defining them stays in Qontak CRM (settings → customization). Out of scope.
- Custom fields for chat actions (
assign,resolve,tag,attachment) — they don't write CRM records; not applicable. - Custom-field types outside the shipped whitelist — the as-built normalizer supports Single-line text, Text Area, Number, Percentage, URL, Dropdown select (see Section 7.1). Any other CRM type — including Date picker, Checkbox, Qontak user, and the array family (Checklist / Multi-select / Tags) — is silently skipped by the lookup and never reaches the picker. Extending the whitelist is a follow-up (D-11), not this PRD.
- Pipeline-scoped or per-pipeline custom-field lists — custom fields are org-level in CRM, so there is nothing to scope: one list serves every pipeline (D-1). The picker deliberately does not filter by the selected pipeline.
- Array (multi-value) custom fields — did not ship and are explicitly out of the as-built scope: values are constrained to scalars (string / number / boolean) at the merge point. Multi-value support is a follow-up (D-11).
- Mekari-product / third-party action custom fields — owned by Mekari Action (HMAC) and Native Integration (OAuth), not this initiative.
Scope Changes
Engineering surfaces this PRD touches (controlled vocab). Kept in sync with the scope_changes frontmatter above.
As-built references verified against chatbot@master + chatbot-fe@main on 2026-07-27.
- Backend —
chatbot:node_executions/nodes/mekari_qontak_crm/execute.rbmerges configured custom-field arguments into the CRM write body as anadditional_fields[]array of{id, name, value, value_name}(merge_additional_field_arguments,execute.rb:249-277), called forWRITE_ACTION_TYPES(execute.rb:33-39);node_resources/mekari_qontak_crm/lookup_resources.rbadds the org-level definitions lookup (additional_field_deal/additional_field_ticket→GET /api/mobile/v2.8/crm/additional_fields?object=…&created_by=user,:18-19,fetch_additional_fields:183-200) plus the type whitelist + descriptor normalizer (ADDITIONAL_FIELD_CONFIG:33-41,build_field_property:202-224); the per-action exposure is anode_registriessettings.option.additional_fieldentry (data seed, not code). - Frontend —
chatbot-fe:modules/shared/ai-agent-actions/composables/useAdditionalFieldLookup.tsfetches the definitions withcompany_idonly — nopipeline_id(:49-59);components/forms/AdditionalFieldSelector.vueis the shipped Add field popover (search + "All fields added" empty state, one flat list, no groups and no type badges);components/forms/ActionIntegrationForm.vuerenders each added field through its existinghtml.element/html.typeswitch and the per-row remove control (:330-338, selector mounted:371-375);PropertiesItem.is_additional_fieldcarries the descriptor (constants/action-constants.ts:28). - Design — Prototyped in
qontak-designer(AddFieldPicker.vue— seedesign-wireframes/ai-agent-custom-fields/). The prototype's pipeline scoping and array rows were not built (D-1, D-11). Production Figma frames still TBD.
6. Constraints
| Field | Value |
|---|---|
| Feature flag | None — descoped (D-12). ai_agent_action_custom_fields was specified (RFC Decision 3.4 + Chunk 1) but never implemented: there is no rollout/ai_agent_action_custom_fields.rb and no flag check anywhere in chatbot or chatbot-fe. The effective gate is the node_registries settings.option.additional_field seed per action — where the seed exists, every org on that action sees the picker. The base ai_agent_action gate (parent PRD Section 6) still applies. |
| Plan / billing component | Inherits the create actions' existing components (parent PRD Section 6): Create Deal → CP-QONTAKCRM-2025-0005 (CRM Deal) → Sales Suite; Create Ticket → CP-QONTAKCRM-2025-0002 (CRM Ticket) → Service Suite. No new billing component — custom fields ride the same entitlement as the action they extend. |
| Platform | Configuration — Qontak web app (AI Agent config drawer). Runtime — the AI Agent function-calls the action during a conversation on any supported channel. |
| Auth | Unchanged — Qontak CRM company token (Bearer), 401 refresh/retry (node_resources/mekari_qontak_crm/crm_http_client.rb:45-77 (401 refresh/retry + Bearer)). Custom fields ride the same authenticated POST. |
| Field scope | Custom fields are org-level in Qontak CRM — the definitions endpoint returns fields scoped to team_id, with no pipeline parameter, so all pipelines in an org share one list (D-1, RFC Decision 3.7). The picker therefore has no pipeline dependency: it loads on mount from company_id alone and works whether Pipeline is Set manually or Let AI decide. |
| Value constraint | Values are scalar only — the merge accepts String, Numeric, or boolean and skips anything else (execute.rb:267), so multi-value/array fields cannot be carried (D-11). For Dropdown select fields the allowed options come from the CRM dropdown[] payload as resource_types: [{type: 'fixed', items: […]}], so both manual selection and AI fill are constrained to real options and the agent cannot invent a value. |
| Performance / data limits | Inherit Qontak CRM API limits. The definitions lookup is a single uncached org-level call (RFC Decision 3.5) expected within the existing pipeline/stage lookup budget (target ≤ 1.5 s to populate the picker); no Qontak-Action-specific quota is introduced. Max custom fields addable per action follows the org's own field count (no separate cap). |
| Data lifecycle | No new persisted artifact beyond the action config itself — the custom-field mapping is stored in the action's node_registry config alongside the existing field mappings; no temp files, jobs, or queues. (4.7 auto-passes.) |
| Read/write | Configure — builder roles that can edit AI Agent config (Admin / Bot Manager). Trigger — the AI Agent at runtime. Customers cannot configure or trigger. |
7. Feature Changes
The delta is entirely additive on the
create deal/create ticketconfig surface + their CRMPOSTbodies. Standard fields and all other actions are untouched.
Change ID: CHG-001 — A dedicated org-level "Add field" picker for custom fields (as-built)
| Field | Detail |
|---|---|
| Change Type | New component (AdditionalFieldSelector.vue) on the action config |
| Surface | AI Agent config → create deal / create ticket → Add field |
| Figma | Pending (prototyped in qontak-designer — AddFieldPicker.vue; shipped UI is simpler — no groups, no badges) |
| Element | Before | After (shipped) |
|---|---|---|
| Custom-field entry point | None — only the hard-coded standard properties are configurable | A separate Add field popover listing the org's custom fields, with a search box; already-added fields drop out of the list |
| Picker scope | N/A | Org-level — one list for the whole org, loaded once on mount from company_id; independent of the Pipeline field's value or mode |
| Picker visibility | N/A | Rendered only when the action's node_registries entry carries settings.option.enabled; hidden entirely once every field has been added (empty state "All fields added") |
| Added field row | N/A | Renders per its normalized descriptor through the existing ActionIntegrationForm.vue switch, with the same Let AI decide / Set manually control as standard fields plus a per-row remove button |
Not shipped from the v1.4 draft: the two-group Standard vs Custom picker with type badges, the "Choose a pipeline first" hint, and stale-field pruning on pipeline change — all three were consequences of the pipeline model that Decision 3.7 retired (D-1).
Change ID: CHG-002 — Scalar custom-field rendering (AI + manual modes) (as-built; array support deferred — D-11)
| Field | Detail |
|---|---|
| Change Type | Reuse of the existing field-rendering switch for normalized custom fields |
| Surface | A custom field added to the create deal / create ticket config |
| Element | Before | After (shipped) |
|---|---|---|
| Text / Text Area / Number / Percentage / URL field | N/A | Renders per its normalized descriptor (input+text, textarea, input+number) with the standard Let AI decide / Set manually toggle |
| Dropdown select field | N/A | Renders as a select over the CRM dropdown[] options (resource_types: [{type:'fixed', items}]); manual pick or AI fill, both constrained to those options |
| Array (Checklist / Multi-select / Tags) field | N/A | Not shipped — absent from the type whitelist, so never offered in the picker; the merge would also drop a non-scalar value (execute.rb:267). Deferred to the follow-up in D-11 |
7.1 Custom-field type → FE component mapping (normalization contract)
Principle: descriptor-driven, not per-type FE code.
ActionIntegrationForm.vueis already an adaptive renderer branching onhtml.element/html.type. The BE lookup normalizes each supported CRM custom-field type into the existingPropertiesItemdescriptor shape (build_field_property,lookup_resources.rb:202-224), and the FE renders it with the switch it already has — no new FE variant per CRM type. A new CRM field type = one row in the BE whitelist, not an FE release.
As-built whitelist — ADDITIONAL_FIELD_CONFIG (chatbot/app/core/repositories/node_resources/mekari_qontak_crm/lookup_resources.rb:33-41). Presence in this map is the type whitelist; the CRM type strings below are the exact keys.
| CRM custom-field type | Normalized descriptor (type / html.element+html.type / extras) | FE control (existing) |
|---|---|---|
Single-line text | string / input + text | MpInput |
Text Area | string / textarea | MpTextarea |
Number | number / input + number | MpInput (number) |
Percentage | number / input + number | MpInput (number) |
URL | string / input + text | MpInput |
Dropdown select | string / select + is_rl: true, resource_types: [{type:'fixed', items}] from CRM dropdown[] | select over fixed items |
| Any other type — incl. Date picker, Checkbox, Qontak user, Checklist / Multi-select / Tags | Not rendered — filter_map skips the field (:187-190), so it never reaches the picker | — (safe degradation) |
Two v1.4 intentions did not survive to as-built: (a) the type coverage is narrower — date, checkbox, user-lookup and the whole array family are not whitelisted (D-11); (b) the skip is silent — no
ai_agent_action_custom_field_type_unsupportedtelemetry was implemented, so an unsupported field is invisible to both the builder and the squad (D-13, OQ A-7).
Change ID: CHG-003 — CRM create body carries mapped custom fields
| Field | Detail |
|---|---|
| Change Type | Modified payload (BE) |
| Surface | POST /api/v3.1/deals, POST /api/v3.1/tickets (and the update counterparts — D-15) |
| Before | Body built only from node-registry standard properties matched by destination (execute.rb:186-208); custom-field args, if any, are dropped |
| After (shipped) | Configured custom fields are collected by merge_additional_field_arguments (execute.rb:249-277) and appended as a single additional_fields[] array, each entry {id, name, value, value_name} where id is the CRM field id from the definitions lookup, name is the config key, and value_name falls back to cached_result_name for dropdowns. use_ai: true reads the agent's argument, otherwise the configured literal is used. Fields with a nil id or a non-scalar value are skipped; an empty result leaves the body untouched. No feature-flag gate (D-12, D-14) |
8. API & Webhook Behavior
Plain-language behavior; exact request/response schema is for the RFC. All calls reuse the existing company-token (Bearer) auth + 401 refresh/retry.
| # | Behavior | Entity Affected | Triggered By | Expected Behavior | Failure Behavior |
|---|---|---|---|---|---|
| 1 | Load the org's custom fields | CRM additional-field definitions (read) | The create deal / create ticket config mounts (no pipeline interaction needed) | One org-level GET …/crm/additional_fields?object=deal|ticket&created_by=user via the resource-lookup endpoint with company_id only; whitelisted types are normalized into field descriptors, the rest skipped | Lookup fails/times out → the request is caught and the picker simply has no options (the Add field button is hidden). No retry affordance and no user-facing error shipped — see D-13 / OQ A-7 |
| 2 | Create deal with custom fields | Qontak CRM deal | Agent function-calls qontak_crm_deal_create with configured custom fields | Existing body (pipeline/stage/name/owner/tags + customer_contact_association lead lookup) plus an additional_fields[] array of {id, name, value, value_name}; dropdown values constrained to the CRM options | 401 → refresh + retry (unchanged); CRM rejects an invalid/missing-required custom field → error surfaces to runtime, no partial silent drop |
| 3 | Create ticket with custom fields | Qontak CRM ticket | Agent function-calls qontak_crm_ticket_create with configured custom fields | Existing ticket body plus the same additional_fields[] array | 401 → refresh + retry (unchanged); CRM rejects invalid/required-missing custom field → error surfaces to runtime |
| 4 | Fill a custom field (runtime) | The custom field's value on the write body | Agent evaluating the conversation for a custom field in Let AI decide | Agent returns a single scalar value; for a dropdown field the value is one of the CRM options | Value absent or non-scalar → the field is dropped from additional_fields[] (silently, execute.rb:266-267); if CRM requires it, the create fails and the validation error surfaces to runtime |
| 5 | Update deal / ticket with custom fields | Qontak CRM deal / ticket | Agent function-calls qontak_crm_deal_update / qontak_crm_ticket_update | As-built: the same merge runs for the update actions (WRITE_ACTION_TYPES), so any custom fields present in that action's config are sent | Same as create; whether a builder can configure them there depends on the action's node_registries seed (D-15, OQ A-6) |
As-built evidence (verified 2026-07-27 against chatbot@master + chatbot-fe@main): definitions lookup node_resources/mekari_qontak_crm/lookup_resources.rb:18-19 (paths), :33-41 (type whitelist), :183-200 (fetch_additional_fields — note it logs Rollbar.error "Failed to fetch <object> additional fields", not the ai_agent_action_custom_field_lookup_failed event this PRD specified), :202-224 (descriptor + fixed-items normalizer). Merge node_executions/nodes/mekari_qontak_crm/execute.rb:249-277, invoked at :90 for WRITE_ACTION_TYPES (:33-39). Unchanged parent behaviour: lead lookup execute.rb:262-276 (enrich_body_for_deal_create), auth node_resources/mekari_qontak_crm/crm_http_client.rb:45-77 (401 refresh/retry + Bearer). FE: modules/shared/ai-agent-actions/composables/useAdditionalFieldLookup.ts:49-59 (company_id-only payload), components/forms/AdditionalFieldSelector.vue (picker), components/forms/ActionIntegrationForm.vue:330-338,371-375. The array primitives the v1.4 draft planned to reuse (PropertiesItem.type:'array', html.element:'select multiple' → ActionMultiSelectField) are NOT wired to custom fields — no array type reaches the picker (D-11) — and ActionMultiSelectField itself now renders via MpInputTag, the control D-3 rejected (D-16).
9. System Flow + User Stories + ACs
9.1 System Flow
Flow: Builder maps the org's custom fields → agent fills them at runtime → BE appends them to the CRM write body · Type: Config journey + runtime fill + API merge
- Builder opens the
create deal/create ticketaction config. No pipeline prerequisite. - On mount the config loads the org's custom-field definitions in one call (
company_idonly); whitelisted types become field descriptors, others are skipped silently. - Builder clicks Add field → a searchable popover lists the org's custom fields not yet added; builder adds one or more.
- For each added field the builder chooses Let AI decide or Set manually; the row renders per the field's descriptor (dropdown → select over CRM options, number → number input, text → input/textarea).
- Builder saves; the mapping is stored in the action's
node_registryconfig. No feature flag is consulted (D-12). - At runtime the agent function-calls the action; for AI-mode fields it returns a scalar value (dropdowns: one of the CRM options).
- BE builds the standard body, then appends
additional_fields[]({id, name, value, value_name}per mapped field) and sends the authenticated write to Qontak CRM. - Decision — value missing or non-scalar: the field is dropped from
additional_fields[]; if every field drops out, the body is sent exactly as it would be without this feature. - Decision — unsupported CRM type: the field never appears in the picker (whitelist miss), with no telemetry and no builder-facing notice (D-13).
- Failure branch: definitions lookup fails → the error is caught, the picker has no options and its Add field button is hidden (no retry UI shipped). CRM rejects an invalid or required-missing custom field → error surfaces to runtime, no silent partial write.
📊 System Flow — Custom fields on create deal / ticket
sequenceDiagram
participant B as Builder
participant Cfg as AI Agent Config (FE)
participant CF as CRM Additional-field Source (org-level)
participant AG as AI Agent (runtime)
participant EX as Action Executor (mekari_qontak_crm)
participant CRM as Qontak CRM API
B->>Cfg: Open create deal / create ticket config
Cfg->>CF: Load org custom fields (company_id only)
alt lookup succeeds
CF-->>Cfg: whitelisted fields (+descriptor, dropdown options)
B->>Cfg: Add field → pick from searchable list, choose AI / manual
else lookup fails
CF-->>Cfg: error (caught)
Cfg-->>B: No options — "Add field" hidden (no retry UI)
end
B->>Cfg: Save (no feature flag)
AG->>EX: function-call create/update deal|ticket (runtime)
EX->>CRM: POST body + additional_fields[] {id, name, value, value_name}
CRM-->>EX: 200 OK
alt 401 Unauthorized
EX->>CRM: refresh company_token + retry
else CRM validation error (invalid / required-missing custom field)
CRM-->>EX: 4xx
EX-->>AG: error surfaced to runtime (no partial write)
end
9.2 User Stories
| User Story | Importance | Mockup | Technical Notes | Acceptance Criteria |
|---|---|---|---|---|
| [QACF-S01] — Org-level custom-field picker (as-built) As a Bot Manager, I want to add my org's CRM custom fields to the create deal/ticket action, so that the agent creates complete CRM records. | Must Have — ✅ shipped | Figma: Pending — shipped as AdditionalFieldSelector.vue (simpler than the qontak-designer AddFieldPicker.vue prototype) | Data Fields: • company_id (number, required) — the only lookup parameter; no pipeline_id (D-1)• custom_fields[] (list) — each { id, name, display_name, type, html, resource_types? } normalized by the BE from the CRM additional-fields responseBefore-After Behavior: Before, only hard-coded standard properties were configurable ( action-constants.ts); after, a separate Add field popover lists the org's custom fields, independent of the Pipeline field. | — Happy Path — • AC-1: Given the action's node_registries entry has settings.option.enabled, when the builder opens the config, then the org's custom fields load in one company_id-scoped call and the Add field control is offered — regardless of whether Pipeline is set manually or left on Let AI decide.• AC-2: ⛔ NOT SHIPPED (D-11) — (drafted: array-typed fields carry a distinct type badge). No array type reaches the picker and the shipped list carries no type badges. • AC-3: Given the builder picks a custom field, when it is added, then it appears as a config row with the standard Let AI decide / Set manually control and a remove button, and it drops out of the picker's remaining options. • AC-4: REPLACED (D-1) — Given Pipeline is on Let AI decide or unset, when the builder opens Add field, then the full org custom-field list is still shown (no gate, no "choose a pipeline first" hint) — custom fields do not depend on pipeline selection. • AC-5: Given the org has a custom field whose CRM type is outside the whitelist (Section 7.1), when the definitions load, then that field is excluded from the picker and never mis-rendered. (⚠️ The ai_agent_action_custom_field_type_unsupported log this AC also required was not implemented — D-13 / OQ A-7.)• AC-6: Given every available custom field has already been added, when the builder looks for Add field, then the control is hidden and the picker's empty state reads "All fields added". — Error / Unhappy Path — • ERR-1: ⚠️ PARTIALLY SHIPPED (D-13) — Given the definitions lookup fails or times out, when the config loads, then the error is caught, the standard fields stay fully usable, and no custom fields are offered. (The drafted retry affordance and the ai_agent_action_custom_field_lookup_failed event were not built; the BE logs Rollbar.error "Failed to fetch <object> additional fields" instead — OQ A-7.)— Permission Model — • CAN: Admin / Bot Manager · CANNOT: roles without AI Agent edit rights · Unauthorized: picker not shown. • Reversibility: adding/removing a field is reversible in config before save; no CRM record is touched at config time. — UI States — • Loading: definitions loading (picker not yet populated) · Empty: no custom fields returned → Add field hidden · All added: "All fields added" · Search miss: "{query}" not found · Success: searchable list rendered |
| [QACF-S02] — Scalar custom field: AI-fill and manual (as-built; multi-value withdrawn to a follow-up — D-11) As a Bot Manager, I want to map a custom field so the agent (or I) can set its value, so that fields like SLA tier, region or account URL are populated on agent-created records. | Must Have — ✅ shipped for scalars | Figma: Pending — shipped via the existing ActionIntegrationForm.vue field switch | Data Fields: • use_ai (boolean) — true = Let AI decide, false = Set manually (execute.rb:259)• value (string | number | boolean) — manual literal or the agent's argument• cached_result_name (string, optional) — the human-readable label sent as value_name for dropdown fields• id (number, required) — CRM field id from the definitions lookup; a field without it is droppedBefore-After Behavior: Before, custom fields could not be mapped at all; after, a mapped custom field renders per its normalized descriptor with the same Let AI decide / Set manually control as every standard field. | — Happy Path — • AC-1: REPLACED (D-11) — Given a Dropdown select custom field in Set manually, when the builder opens its value control, then it offers exactly the CRM dropdown[] options and stores the chosen option (single value).• AC-2: REPLACED (D-11) — Given a custom field in Let AI decide, when configured, then no manual value control is required and the value is taken from the agent's argument at runtime. • AC-3: REPLACED (D-11) — Given a Dropdown select field in Let AI decide, when the agent runs, then the value sent is a single option value and value_name carries its label.• AC-4: Given a text / textarea / number / percentage / URL custom field, when added, then it renders per its type ( input+text, textarea, input+number) with the standard Let AI decide / Set manually control.• AC-5: Given any mapped custom field, when the agent supplies a non-scalar value (array/object), then that field is omitted from additional_fields[] rather than sent malformed (execute.rb:267).— Error / Unhappy Path — • ERR-1: Given a mapped custom field whose resolved value is nil, when the action runs, then the field is skipped and the rest of the body is unaffected; if all fields are skipped the body is byte-identical to the no-custom-field case.— Permission Model — • CAN: Admin / Bot Manager · CANNOT: roles without AI Agent edit rights · Unauthorized: control not shown. — UI States — • Loading: dropdown options loading · Empty: dropdown with no options → no fixed items · Success: control rendered per descriptor |
| [QACF-S03] — Merge mapped custom fields into the CRM write (as-built) As the AI Agent runtime, I want the configured custom fields sent with the deal/ticket write call, so that the created record carries the org's custom-field values. | Must Have — ✅ shipped | Figma: N/A — backend | Data Fields: • additional_fields[] — array of {id, name, value, value_name} appended to the body (execute.rb:271)• existing standard body fields (unchanged) Before-After Behavior: Before, extract_arguments_by_destination (execute.rb:186-208) builds the body only from node-registry standard properties and drops custom-field args; after, merge_additional_field_arguments (:249-277) appends the additional_fields[] array for every action in WRITE_ACTION_TYPES. | — Happy Path — • AC-1: REPLACED (D-12, D-14) — Given custom fields are mapped, when the agent calls qontak_crm_deal_create / qontak_crm_ticket_create, then the body carries an additional_fields[] array with one {id, name, value, value_name} entry per mapped field, alongside the unchanged standard fields — with no feature-flag check (the flag was never implemented).• AC-2: REPLACED (D-11) — Given a mapped field's value is a scalar (string / number / boolean), when merged, then it is sent as-is; a non-scalar value is skipped instead of being serialized. • AC-3: REPLACED (D-12) — Given an action config with no custom fields mapped (every existing pre-feature config), when the agent calls the action, then additional_fields is absent and the body is byte-identical to the parent behaviour.• AC-4: Given the existing customer_contact_association and {{customer_name}} behavior, when a create runs with custom fields, then that existing behavior still applies unchanged (execute.rb:262-276 (enrich_body_for_deal_create)).• AC-5: NEW (D-15) — Given qontak_crm_deal_update / qontak_crm_ticket_update, when the agent calls them with custom fields present in the config, then the same additional_fields[] merge applies (the executor gates on WRITE_ACTION_TYPES, not on create-only).— Error / Unhappy Path — • ERR-1: Given CRM returns 401, when the create runs, then the company token is refreshed and retried (unchanged). • ERR-2: Given CRM rejects the create because a custom-field value is invalid or a required custom field is missing, when it runs, then no partial/silent write occurs and the validation error surfaces to the runtime. — Permission Model — • CAN: the AI Agent at runtime · CANNOT: customers cannot trigger · Unauthorized: not exposed where the base action is off. • Reversibility: a created record cannot be undone by the agent — editable/deletable only in Qontak CRM (unchanged from parent). — UI States — • N/A — backend/runtime behavior |
| [QACF-S04] — Prune stale custom fields on pipeline change ⛔ WITHDRAWN 2026-07-27 (D-1) — custom fields are org-level, so a pipeline change can never make a mapped field stale. Nothing was built and nothing needs to be. Its Jira Story should be closed Won't Do, not left open. | Withdrawn | — | — | — Withdrawn — • AC-1: ⛔ withdrawn — no pipeline-owned field set exists to diff. • AC-2: ⛔ withdrawn — the picker header carries no pipeline label. • ERR-1: ⛔ withdrawn — no pruning path exists, so no orphaned mapping can be produced this way. |
| [QACF-S01-NEG] — Scope guard rails (Guard Rail — from Non-Goals) As a builder, I should not find this custom-field surface where it isn't in scope. | Guard Rail | — | — | — Negative Scenarios — • NEG-1: ⚠️ BE-CONTRADICTED (D-15) — Given the update deal / update ticket config, when the builder looks for the custom-field picker, then it is offered only if that action's node_registries entry carries settings.option.additional_field; the BE merge itself already accepts custom fields on both update actions, so this guard rail is a seed/config guarantee, not a code guarantee. QA must verify against the live node_registries rows (OQ A-6).• NEG-2: REPLACED (D-1) — Given Pipeline is on Let AI decide, when the builder opens Add field, then the org's custom fields are listed (no gate) — the previous "choose a pipeline first" expectation is void. • NEG-3: REPLACED (D-1) — Given the picker, when the builder compares two pipelines, then both show the same org-level custom-field list — there is no per-pipeline subset to withhold. • NEG-4: Given the picker, when the builder wants to create/edit a custom-field definition, then it is not possible here — definitions live in Qontak CRM settings → customization (Non-Goal 2). • NEG-5: NEW (D-11) — Given an org with Checklist / Multi-select / Tags / Date / Checkbox custom fields in CRM, when the builder opens Add field, then those fields are not listed (whitelist miss) and no error explains their absence. |
Dependencies: [QACF-S02] depends on [QACF-S01] (the picker). [QACF-S03] (BE merge) depends on the config produced by S01/S02 and on the BE TECH-RFC. [QACF-S04] is withdrawn (D-1) — close its Jira Story as Won't Do. Story→Jira: one Story per QACF-S0x under child Epic BOT-4662 (see ADR 0001 for the Story-family remediation).
🧪 Test Coverage Matrix — [QACF-S01]
| Dimension | Coverage | Notes |
|---|---|---|
| Boundary values | ✅ defined | AC-6 all-added empty state; ⚠️ QA: org with 0 custom fields (Add field hidden) and very large custom-field count (popover scroll) |
| State transitions | ✅ defined | AC-1/AC-3 picker → added row; AC-4 pipeline-independent load |
| Data validation | ⚠️ TBD | ⚠️ QA: custom-field label/name with special chars; two CRM fields sharing a name (the picker de-dupes by name, so a collision would hide one) |
| Concurrency | ⚠️ TBD | ⚠️ QA: action saved while the definitions lookup is still in flight (fields not yet loaded) |
| Network/timeout | ⚠️ partial | ERR-1 lookup failure degrades silently — QA must assert the standard fields stay usable and that no retry/error UI is expected (D-13) |
🧪 Test Coverage Matrix — [QACF-S02]
| Dimension | Coverage | Notes |
|---|---|---|
| Boundary values | ⚠️ partial | AC-5 non-scalar dropped; ⚠️ QA: dropdown with a single option; very long text value; numeric field given a non-numeric AI value |
| State transitions | ✅ defined | AC-2 AI ↔ AC-1 manual mode toggle |
| Data validation | ✅ defined | AC-1/AC-3 dropdown constrained to CRM options; AC-5 non-scalar skipped; ERR-1 nil skipped |
| Concurrency | ⚠️ TBD | ⚠️ QA: CRM dropdown options edited between config-save and runtime (stale value/value_name) |
| Network/timeout | ⚠️ TBD | ⚠️ QA: definitions load timeout on a config that already has fields mapped |
🧪 Test Coverage Matrix — [QACF-S03]
| Dimension | Coverage | Notes |
|---|---|---|
| Boundary values | ⚠️ partial | ERR-1 nil skipped; AC-5 update actions; ⚠️ QA: max additional_fields[] length CRM accepts; field id present but deleted in CRM |
| State transitions | ✅ defined | AC-3 no fields mapped → parent body; AC-1 fields mapped → additional_fields[] appended |
| Data validation | ✅ defined | AC-2 non-scalar skipped; ERR-2 CRM rejects invalid/required-missing custom field |
| Concurrency | ⚠️ TBD | ⚠️ QA: agent fires create twice in rapid turns (inherits parent QACT-S01 concern) |
| Network/timeout | ✅ defined | ERR-1 401 refresh + retry (unchanged) |
10. Rollout
As-built: there is no staged flag rollout. The ai_agent_action_custom_fields flag was descoped (D-12), so the effective release control is the node_registries settings.option.additional_field seed per action — once seeded, the picker is live for every org already entitled to that action. The three stages below are recorded as the drafted plan and what actually applied:
- Internal QA —
enable the flag on an internal test org→ verified on an internal org by seeding the action'snode_registriesoption. Config → runtime fill → CRM merge exercised for scalar + dropdown fields only (no array types exist to test — D-11). - Closed Beta — skipped in practice: with no per-org flag there was no mechanism to limit exposure to 3–5 design partners. Recorded as a deliberate consequence of D-12, not an oversight to repeat.
- GA — completed 2026-07-27: enabled for all clients/users with the create-deal/ticket entitlement, effective at seed time.
GA record (from v1.5, corrected). CRM Custom Fields reached 100% rollout to all clients/users on 2026-07-27; the implementation is recorded in the Chatbot BE 1.324.0 and Hadiningbot FE 3.8.3 release records. ⚠️ v1.5 also stated "the feature flag remains the operational control for emergency rollback" — that is not true and is corrected here: no such flag exists in chatbot or chatbot-fe (D-12). Emergency rollback today requires a code revert and deploy, not a console toggle — anyone relying on the flag during an incident would lose time discovering this. Fixing the capture note (2026-07-27-crm-custom-fields-general-availability.md) is part of OQ A-8.
Migration transition window: the only stored artifact is the action config. Existing create deal/create ticket actions configured before this shipped carry no custom-field mapping, so merge_additional_field_arguments returns the body untouched and they behave exactly as before (AC-3 of [QACF-S03]). No data backfill and no migration of existing records is required; old and new configs coexist safely.
Rollback: with no flag, rollback means un-seeding the action's
node_registriesoption (hides the picker; already-saved mappings would then be sent by the executor but no longer be editable) or reverting the BE merge. Worth deciding deliberately before the next action gains custom fields — see OQ A-8.
11. Observability
⛔ As-built: none of the five events below shipped (D-13). The only telemetry in the shipped path is a generic
Rollbar.error(e, message: "Failed to fetch <deal|ticket> additional fields", organization_id:)in the lookup's rescue (lookup_resources.rb:198). There is no adoption signal, no merge signal, no rejection signal, and unsupported field types are dropped silently. Consequence: every metric in Section 12 is currently unmeasurable and the alert below does not exist. Tracked as OQ A-7.
| Event | Trigger | Key properties | Shipped? |
|---|---|---|---|
ai_agent_action_custom_field_added | Builder adds a custom field in config | action_type (deal/ticket), field_type, mode (ai/manual) — (pipeline_id/is_array dropped: D-1, D-11) | ⛔ No |
ai_agent_action_custom_field_lookup_failed | Org custom-field lookup errors/times out | organization_id, object (deal/ticket), latency_ms, error | ⚠️ Partial — generic Rollbar error only, no event name, no latency |
ai_agent_action_custom_field_merged | Custom fields sent with a CRM write at runtime | action_type, field_count | ⛔ No |
ai_agent_action_custom_field_create_rejected | CRM rejects a write due to a custom-field value | action_type, reason (invalid / required-missing) | ⛔ No |
ai_agent_action_custom_field_type_unsupported | Lookup returns a CRM field type outside the whitelist (S7.1) — field excluded from picker | organization_id, raw_type, field_name | ⛔ No — silent filter_map skip |
Dashboard owner: BOT — Chatbot & AI Squad. Alert (not yet live): if ai_agent_action_custom_field_create_rejected / total custom-field writes > 5% over any rolling day, page the squad (signals an options or required-field contract problem).
11.1 Post-Launch Monitoring Cadence
Weekly review for the first 4 weeks post-GA. If the custom-field create-rejection rate exceeds 5% week-over-week, or lookup-failure rate exceeds 2%, investigate immediately before widening rollout. Blocked today — the events this cadence reads do not exist (D-13); until OQ A-7 is closed, post-launch monitoring is limited to Rollbar noise on the lookup and manual spot-checks of created records.
12. Success Metrics
Baselines are N/A — and targets remain unmeasurable post-GA because the instrumentation in Section 11 did not ship (D-13). Closing OQ A-7 is the precondition for reporting any row below.
| Category | Metric | Baseline | Target | Measurable today? |
|---|---|---|---|---|
| ⭐ Adoption | % of active create deal/create ticket actions that map ≥1 custom field | N/A (0 — not possible before this shipped) | ≥ 40% within 60 days of GA | ⚠️ Only by querying node_registry action configs directly (no event) |
| Quality | Custom-field write-rejection rate (rejected ÷ custom-field writes) | N/A | < 2% steady-state (alert at 5%) | ⛔ No |
| Efficiency | Reduction in manual custom-field back-fill on agent-created records (design-partner self-report + CRM edit-after-create rate) | Manual back-fill on ~100% of custom-field records | ≥ 70% reduction for beta orgs within 30 days | ⚠️ Self-report only (no closed beta ran — D-12) |
| Reliability | Custom-field definitions lookup success rate | N/A | ≥ 99% (≤ 1.5 s p95) | ⚠️ Failure count only, via Rollbar; no latency |
13. Launch Plan & Stage Gates
| Stage | Audience | Duration | Success gate |
|---|---|---|---|
| Internal QA | 1 internal org with org-level custom fields | ~1 week | ✅ Met for the shipped surface — config → runtime → CRM merge verified for scalar + dropdown fields; no-mapping path byte-identical to parent. (Array coverage void — D-11.) |
| Closed Beta | 3–5 design-partner orgs | 2–3 weeks | ⛔ Not run — no per-org gate existed after the flag was descoped (D-12) |
| GA | All orgs w/ create-deal/ticket entitlement | — | ⚠️ Reached with two gates unmet: monitoring + alert are not live (D-13) and the beta gate above was skipped. Recorded rather than back-dated; OQ A-7 is the remediation |
14. Dependencies
| Dependency | Owner | What it provides | Blocking |
|---|---|---|---|
| BE payload merge + custom-field source (TECH-RFC) | BOT — Chatbot & AI Squad (chatbot) | ✅ Delivered — (a) merge_additional_field_arguments appends additional_fields[] (execute.rb:249-277); (b) per-action exposure via the node_registries settings.option.additional_field seed; (c) the org-level definitions lookup + type whitelist + descriptor normalizer (lookup_resources.rb:18-19,33-41,183-224). (Not delivered from the RFC: the flag rollout class — Chunk 1 / D-12.) | YES — was blocking; now closed |
FE picker + field rendering (chatbot-fe) | BOT — Chatbot & AI Squad | ✅ Delivered — AdditionalFieldSelector.vue (searchable Add-field popover) + useAdditionalFieldLookup.ts + per-row rendering/removal in ActionIntegrationForm.vue. (Not delivered: Standard/Custom grouping, type badges, array controls.) | YES — was blocking; now closed |
| Qontak CRM API accepts custom fields on write | Qontak CRM (platform) | ✅ Confirmed 2026-07-15 (RFC A-1): definitions via GET /api/mobile/v2.8/crm/additional_fields?object=deal|ticket&created_by=user (org/team_id-scoped); values sent as additional_fields[] entries keyed by field id (+ name, value, value_name) | YES — closed |
| Design — production Figma frames | Design | Frame-level specs for the shipped Add-field popover + field rows | NO — shipped against the prototype; Figma still outstanding as as-built documentation |
qontak-designer prototype (reference) | PM / Design | Working reference for the picker — design-wireframes/ai-agent-custom-fields/. Stale in two ways: it models pipeline scoping and array rows, neither of which shipped (D-1, D-11) | NO — reference only; flag as stale for the designer |
📊 Dependency Graph — Custom fields on create deal / ticket
graph LR
F[Custom fields on create deal/ticket]
F -->|BLOCKING - closed| BE[BE merge + org-level lookup TECH-RFC]
F -->|BLOCKING - closed| FE[FE Add-field picker + field rows]
F -->|BLOCKING - closed| CRMAPI[CRM API - additional_fields by field id]
F -->|non-blocking| DES[Production Figma frames - outstanding]
F -->|non-blocking| PROTO[qontak-designer prototype - stale]
15. Key Decisions + Alternatives Rejected
Decisions Made
| # | Decision | Rationale | Date |
|---|---|---|---|
| D-1 | AMENDED 2026-07-27 — custom fields are org-level, not pipeline-scoped. The picker lists the org's whole additional-field set and is independent of the Pipeline field. (Superseded: "custom fields are scoped to the selected pipeline".) | The CRM contract, verified 2026-07-15, settles it: GET /api/mobile/v2.8/crm/additional_fields?object=deal&created_by=user returns fields scoped to team_id with no pipeline parameter, so every pipeline in an org shares one definition set. A pipeline filter would have withheld data the builder is always entitled to see, and CRM offers no server-side way to apply one. PM ratified RFC Decision 3.7 Option A — closes RFC open question A-4. | 2026-07-27 |
| D-2 | WITHDRAWN 2026-07-27 — no "Pipeline set manually" prerequisite. The definitions load on mount from company_id; Let AI decide on Pipeline no longer gates anything. (Superseded: "custom fields require Pipeline set manually, gated with a hint otherwise".) | Follows directly from D-1 — with org-level fields there is nothing a concrete pipeline would resolve. Removing the gate deletes a whole class of UX state (hint, prune, re-scope) rather than moving it. | 2026-07-27 |
| D-3 | VOID 2026-07-27 (see D-11, D-16) — array fields were to reuse the existing prod multi-select primitive (ActionMultiSelectField, html.element:'select multiple', type:'array'), and MpInputTag was explicitly rejected | Doubly overtaken: no array type was whitelisted so nothing was wired to it (D-11), and the primitive itself was re-implemented on the rejected MpInputTag (D-16). Do not treat this row as the design of record for the follow-up without re-deciding the control. | 2026-07-13 |
| D-4 | NARROWED 2026-07-27 — AI-mode values are constrained to allowed options for Dropdown select fields, via resource_types: [{type:'fixed', items}] built from the CRM dropdown[] payload | The constraint principle shipped, but only where CRM supplies an option list. Free-text/number/URL custom fields accept whatever the agent produces, bounded only by the scalar check at merge time. | 2026-07-27 |
| D-5 | SUPERSEDED 2026-07-27 (see D-12) — gate behind a new flag ai_agent_action_custom_fields (default OFF) | The flag was specified in the PRD and in RFC Decision 3.4 + Chunk 1, then descoped during delivery. | 2026-07-13 |
| D-6 | Scope to create actions only | Highest-value, unblocks the current gap; update actions are a clean fast-follow reusing the same picker + merge. | 2026-07-13 |
| D-7 | Type→component mapping is a BE normalization contract, not per-type FE code | Tenants define custom fields in CRM first, so the variant space is open-ended. The BE lookup normalizes every CRM type into the existing PropertiesItem descriptor shape (Section 7.1) and the FE renders it with the adaptive switch it already has (ActionIntegrationForm.vue branches on html.element/html.type). A new CRM type = one normalizer row, not an FE release. Unknown types degrade safely: excluded from the picker + telemetry, never mis-rendered. | 2026-07-13 |
| D-8 | R-1 mitigation confirmed by PM: required custom fields must be surfaced/flagged in the picker so the builder is forced to map them; runtime rejection (S03/ERR-2) surfaces cleanly rather than silently dropping | Aligns with the drafted mitigation — resolves R-1. Exact required-field detection mechanism still confirmed in the RFC (depends on A-1's CRM API answer). | 2026-07-13 |
| D-9 | R-2 confirmed: no artificial cap on AI array cardinality — the agent returns as many valid allowed-option values as the conversation supports | PM confirmed the default; constrained-to-allowed-options (D-4) already bounds the risk. Revisit only if Beta shows real over-population. | 2026-07-13 |
| D-10 | PARTLY OVERTAKEN 2026-07-27 (see D-15) — update deal / update ticket custom fields were declared a separate future PRD | Still the right split for the builder-facing surface and its ACs, but the shipped BE merge already covers both update actions, so the "future PRD" now inherits working plumbing and only needs the config surface + ACs. | 2026-07-13 |
| D-11 | Array-typed custom fields (Checklist / Multi-select / Tags) did NOT ship — deferred to a follow-up. The as-built whitelist is scalar-only (Single-line text, Text Area, Number, Percentage, URL, Dropdown select); Date picker, Checkbox and Qontak user are also absent. The merge additionally hard-rejects non-scalar values (execute.rb:267). | Recorded, not rationalised: array support was a Must Have in v1.4 ([QACF-S02]) and one of the two headline capabilities of this adjustment. Delivery whitelisted the six types CRM returned in the confirmed payload and left the array family out; the FE primitives to render it exist and are unused. This is a real scope reduction the PRD must show, and it needs a PM decision (OQ A-5) — either a follow-up PRD or an explicit permanent non-goal. | 2026-07-27 |
| D-12 | The ai_agent_action_custom_fields flag did NOT ship — the feature is ungated. No rollout/ai_agent_action_custom_fields.rb, no flag check in chatbot or chatbot-fe. Release control is the per-action node_registries settings.option.additional_field seed. | RFC Chunk 1 specified the rollout class; the corresponding task (BOT-4668) was closed Won't Fix (0.5 md descoped). Consequences accepted here: no closed beta was possible, and rollback is un-seeding or a revert (OQ A-8). Low blast radius because a config with no mapped fields is byte-identical to the parent behaviour. | 2026-07-27 |
| D-13 | No custom-field telemetry shipped. None of the five Section 11 events exist; unsupported types are skipped silently; the lookup's only signal is a generic Rollbar.error. | Recorded as a gap, not a choice: it makes every Section 12 metric unreportable and leaves the "unsupported CRM type" path invisible to both builder and squad — exactly the failure mode AC-5 was written to prevent. Remediation is OQ A-7. | 2026-07-27 |
| D-14 | Payload shape: one additional_fields[] array of {id, name, value, value_name}, keyed by the CRM field id, appended to the write body — not individual top-level keys as v1.4 assumed | Matches what the CRM write API accepts (RFC A-1, confirmed 2026-07-15). value_name carries the human-readable label for dropdown values (cached_result_name), which keeps the payload self-describing for CRM-side display. | 2026-07-27 |
| D-17 | The custom-field picker is not AI-Agent-only — it also renders on the bot-automation action drawers. CreateActionForm.vue calls the same useAdditionalFieldLookup(settings) and is mounted by modules/bot-automation/components/drawers/NewActionDrawer.vue and EditCapabilityActionDrawer.vue, each passing tool.node_registry.settings unchanged. | Verified 2026-07-27. Consequence: exposure is governed per node_type only — one seed lights the field up on every surface that renders that action, so "AI Agent only" is not expressible in the current design. This PRD scoped itself to the AI Agent config drawer and never specified the bot-automation surface (no ACs, no QA, no design). Tracked as OQ A-11. | 2026-07-27 |
| D-16 | ActionMultiSelectField was re-implemented on MpInputTag (BOT-4724) — reversing D-3's explicit rejection of it. The component file name survives, but its internals are now MpInputTag (ActionMultiSelectField.vue:2,25), and three follow-up fix commits (353c6692, eedbc7d4, 5af88261) followed the swap. | Verified 2026-07-27. Matters for two reasons beyond bookkeeping: (a) D-3 and RFC FE-D4 both named MpInputTag as the rejected option, so a rejected alternative shipped without either doc being amended; (b) the component is shared with the already-shipped deal tags field, so the blast radius is wider than this PRD. It does not affect the custom-field path today only because no array type reaches it (D-11). Raised by the completion-alignment audit (Direction B #4) and re-verified independently here. | 2026-07-27 |
| D-15 | The BE merge is wired for all four write actions (qontak_crm_deal_create, qontak_crm_deal_update, qontak_crm_ticket_create, qontak_crm_ticket_update — WRITE_ACTION_TYPES, execute.rb:33-39), not create-only | Engineering chose the general gate, which is cheap and forward-compatible with the update-actions follow-up. The product consequence must be understood: the create-only boundary (Non-Goal 1 / NEG-1) is now enforced only by which actions get the node_registries seed. PM to confirm the seed state per action (OQ A-6). | 2026-07-27 |
Alternatives Rejected
| Alternative | Why rejected | Date |
|---|---|---|
| The original rejection assumed per-pipeline definitions. There is no union to build: CRM defines one org-level set, so the "union" is the correct list and the pipeline-scoped alternative was the wrong model. | 2026-07-27 | |
| Nothing is inferred because nothing is pipeline-dependent: the same list loads whatever the Pipeline field's mode. | 2026-07-27 | |
| Keep the pipeline gate anyway (RFC Decision 3.7 Option B) | Would have kept the PRD unamended at the cost of shipping a knowingly wrong model — withholding fields the builder can always use, with no CRM-side filter to implement it honestly (client-side only). Rejected in favour of correcting the PRD. | 2026-07-27 |
| Back-date the PRD to pretend arrays, the flag and the telemetry were never in scope | They were: arrays and the flag were Must-Have/committed in v1.4, and the telemetry was the basis of every success metric. Silently rewriting scope would erase the delivery signal (a Must-Have dropped without a decision) and leave the metrics section quietly unmeasurable. Recorded as D-11 → D-13 with owners instead. | 2026-07-27 |
| Free-text values for array fields | Would let the agent produce values CRM rejects (invalid options), reintroducing the failure this PRD removes. Constrained-to-allowed-options is required. | 2026-07-13 |
| Build a bespoke multi-value component | Unnecessary — the array primitives already exist in chatbot-fe; a new component adds maintenance and inconsistency. | 2026-07-13 |
| Fold this into the parent as-built PRD | The parent records shipped behavior; this is net-new capability with its own ACs, RFC, and rollout — it belongs in its own ADJUSTMENT PRD under the same Epic. | 2026-07-13 |
| Cap AI array cardinality with a per-field max | PM confirmed (D-9) no artificial cap is needed at launch — constrained-to-allowed-options already bounds the risk; a cap would need arbitrary tuning with no evidence yet. | 2026-07-13 |
| Silently expand this PRD's scope to cover update actions | PM confirmed (D-10) update actions stay a separate future PRD — keeps this ADJUSTMENT's delta precise and its regression surface narrow (CB-A.2). | 2026-07-13 |
16. Open Questions
| Type | Item | Owner | Deadline | Mitigation (required for Risk) |
|---|---|---|---|---|
| Open Question | ✅ RESOLVED 2026-07-15 (RFC A-1; see D-1, D-14) — A-1 — CRM custom-field API contract: (a) definitions come from GET /api/mobile/v2.8/crm/additional_fields?object=deal|ticket&created_by=user and are org-level (team_id), with no per-pipeline endpoint; (b) values are sent by field id inside an additional_fields[] array. The array-payload half of the question is moot for now — no array type shipped (D-11). | BE + Qontak CRM | Before RFC | — |
| Risk | ✅ RESOLVED 2026-07-13 (see D-8) — R-1 — Required custom fields: if a pipeline has a required custom field and neither AI nor manual supplies it, CRM rejects the create at runtime. | PM + BE | Before Beta | Surface a pipeline's required custom fields in the picker (e.g. auto-added / flagged) so the builder must map them; runtime error (S03/ERR-2) surfaces cleanly rather than silently dropping. PM confirmed this mitigation (D-8); exact required-field detection mechanism still confirmed in the RFC (depends on A-1). |
| Risk | ✅ RESOLVED 2026-07-13 (see D-9) — R-2 — AI array cardinality: unbounded AI value selection could over-populate an array field. | PM | Before Beta | Confirmed: AI returns as many valid allowed-option values as the conversation supports, no artificial cap. Constrained-to-allowed-options (D-4) bounds the blast radius. |
| Assumption | A-2 — Lookup performance: the org-level custom-field lookup returns within the existing pipeline/stage lookup budget (≤ 1.5 s p95). Still unverified — no latency telemetry shipped (D-13), and the lookup is uncached by design (RFC Decision 3.5). | BE | Post-GA | If it exceeds budget, add the short-TTL Redis cache the RFC pre-approved. Cannot be assessed until A-7 lands. |
| Open Question | ✅ RESOLVED 2026-07-13 (see D-10, D-15) — A-3 — Update actions fast-follow: confirm update deal / update ticket custom fields are a follow-up PRD (reusing this picker + merge), not this one. | PM | Post-GA | Confirmed — separate future PRD. Note the BE half already ships (D-15), so the follow-up is config-surface + ACs only. |
| Open Question | ✅ RESOLVED 2026-07-27 (see D-1, D-2) — A-4 (raised by the BE RFC) — org-level vs pipeline-scoped: should the picker show custom fields without requiring a pipeline selection, and is PRD D-1/D-2 to be amended? | PM | Before FE sprint | Ratified: org-level, no pipeline gate. D-1 amended, D-2 withdrawn, [QACF-S04] withdrawn, AC-4 / NEG-2 / NEG-3 replaced, Non-Goals 5–6 rewritten. Mirrored back into the RFC (A-4 closed). |
| Decision needed | A-5 — Array custom fields (D-11): arrays were a v1.4 Must Have and did not ship. Choose: (a) follow-up PRD to extend the whitelist + wire the existing array primitives (also picks up Date / Checkbox / Qontak user), or (b) declare arrays a permanent non-goal for the AI Agent and tell CRM-heavy orgs so explicitly. | PM | Next planning cycle | Interim expectation-setting: the picker silently omits those fields today, so a builder cannot tell they are unsupported (NEG-5). At minimum, document the supported-type list in the customer-facing feature guide. |
| Decision needed | A-6 — Exposure surface (D-15, D-17): the exposure question is wider than create-vs-update. It is 4 node types × 3 UI surfaces, all driven by the same per-node-type seed with no per-surface control: node types qontak_crm_deal_create / qontak_crm_deal_update / qontak_crm_ticket_create / qontak_crm_ticket_update; surfaces = AI Agent action drawer, and the two bot-automation drawers (D-17). Confirm which node types carry settings.option today. This is checkable without DB access — see the runbook below. | PM + BE | Immediate | If a node type is seeded beyond this PRD's scope, decide: un-seed it, or fast-track the follow-up PRD's config-surface ACs. Note un-seeding hides the picker but does not stop the executor from sending mappings already saved (A-8). |
| Risk | A-11 — Custom fields reach the bot-automation surface with no spec (D-17): the picker is not AI-Agent-only. CreateActionForm.vue calls the same useAdditionalFieldLookup and is rendered by modules/bot-automation/components/drawers/NewActionDrawer.vue (:194) and EditCapabilityActionDrawer.vue (:165,:194), both passing tool.node_registry.settings straight through. So a bot-tree/capability action on a seeded node type gets custom fields too — a surface this PRD never mentions, with no ACs, no QA and no design. | PM + BE | Immediate | Confirm on staging whether the bot-automation drawers currently show the picker for the CRM actions. If yes, it needs either its own ACs or a surface-level guard; a per-node-type seed cannot express "AI Agent only". |
| Risk | A-7 — Zero telemetry (D-13): none of the five specified events exist, so adoption, rejection rate, lookup latency and unsupported-type occurrences are all unobservable, and the 5% rejection alert does not exist. | BOT squad + PM | Before widening to more actions | Land the five events (a small BE/FE task each) or explicitly downgrade Section 12 to "not measured". Until then post-GA monitoring is Rollbar noise + manual record spot-checks. |
| Risk | A-9 — Lane-B initiative live with zero QA coverage: the completion-alignment audit found 0 test cases for this scope in either system — no chatbot/qontak-action/testcases/ suite and no Pokayoke case referencing any in-scope Jira key (verified against a known-good control key). All 6 delivered ACs are uncovered, including the two money-critical error paths (S03/ERR-1 401 retry, S03/ERR-2 CRM rejection), while its QA task BOT-4688 is Done. | PM + QA (Tito Amanda / Izzul) | Immediate | Author the missing cases (qa-test-cases-creation) against the amended ACs in this version — the audit notes no existing case needs correcting because none exists. Also decide whether the five Stories BOT-4663–4667 (all still To Do / In Progress post-GA) are transitioned or the GA claim qualified. |
| Risk | A-10 — Rejected control shipped (D-16): ActionMultiSelectField now renders via MpInputTag, which D-3 and RFC FE-D4 explicitly rejected, and the component is shared with the already-shipped deal tags field. | BOT squad | Before the array follow-up | Confirm the deal tags field is unaffected in production, then either amend D-3/FE-D4 to accept MpInputTag deliberately or restore the mandated control. Do not start the array follow-up on an undecided control. |
| Risk | A-8 — No kill switch (D-12): with the flag descoped, disabling the feature means un-seeding node_registries (which hides the picker but leaves already-saved mappings being sent) or reverting the BE. | BOT squad | Before the next action gains custom fields | Either restore the RFC Chunk-1 rollout class, or document un-seed-plus-config-cleanup as the accepted rollback runbook. |
16.1 A-6 / A-11 verification runbook (no DB access needed)
The node_registries seed is database state with no trace in db/ or lib/, which is why the audit scored exposure unverifiable from the repos. It is still checkable from outside: the same rows the FE reads are served by GET https://chatbot-staging.qontak.com/api/v1/node_registries (production: https://chatbot.qontak.com/api/…), role-gated to owner / supervisor / admin and scoped to the caller's organization. The response's settings is the raw row hash — exactly what the drawer branches on.
# 1. Exposure per node type (answers A-6)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://chatbot-staging.qontak.com/api/v1/node_registries?limit=200" \
| jq '.data[]
| select(.node_type | test("^qontak_crm_(deal|ticket)_(create|update)$"))
| {node_type,
picker_shown: (.settings.option.enabled == true),
has_field_source: (.settings.option.additional_field != null),
resource_key: .settings.option.additional_field.resource_types[0].resource_key}'
| Result per node type | Reading |
|---|---|
picker_shown: true on a create action | Expected — this PRD's scope |
picker_shown: true on an update action | A-6 fires: live beyond scope, no ACs/QA. Contradicts Non-Goal 1 / NEG-1 |
picker_shown: false but fields already mapped in a saved config | Picker hidden, yet the executor still sends them (A-8) — un-seeding is not a kill switch |
has_field_source: false while enabled: true | Picker mounts with no options and self-hides — indistinguishable from a failed lookup (D-13) |
FE branch points this mirrors: ActionIntegrationForm.vue:371 (requiredItem?.settings?.option?.enabled) and useAdditionalFieldLookup.ts:38-45 (option.enabled → additional_field.is_rl → resource_types[0].provider/resource_key).
For A-11 (bot-automation surface) the API cannot answer it — the seed is per node_type, not per surface, so any picker_shown: true above implies the picker on every surface rendering that action. Confirm by clicking: open bot automation → add/edit a capability action → pick the CRM action (NewActionDrawer / EditCapabilityActionDrawer) and check whether Add field appears. If it does, the feature is live on a surface this PRD never specified.
PRD CHANGELOG
| Version | Date | Author | Section(s) | Type | Summary |
|---|---|---|---|---|---|
| 1.7 | 2026-07-27 | Dimas Fauzi Hidayat | 15 (D-17), 16 (A-6, A-11, new 16.1) | ADDED | Exposure surface investigated (A-6). Two findings. (1) D-17 / A-11 — the picker is not AI-Agent-only: CreateActionForm.vue calls the same useAdditionalFieldLookup and is mounted by the bot-automation drawers NewActionDrawer.vue:194 and EditCapabilityActionDrawer.vue:165,194, both passing tool.node_registry.settings through, so a seeded node type lights the field up on the bot-tree/capability surface too — no ACs, no QA, no design. Exposure is expressible per node_type only; "AI Agent only" cannot be configured. (2) A-6 is now checkable without DB access — new §16.1 runbook: GET /api/v1/node_registries serves the same settings rows the drawer branches on, with a jq filter over the four qontak_crm_* node types, a reading table per outcome, and the click-path for the surface question the API cannot answer. |
| 1.6 | 2026-07-27 | Dimas Fauzi Hidayat | Header, 2, 3, 4, 5, Scope Changes, 6, 7, 7.1, 8, 9, 10, 11, 12, 13, 14, 15, 16 | AS-BUILT + RESOLVED | Reconciled to the shipped implementation and the merged BE RFC, answering open questions 1–4 of the completion-alignment audit (⛔ NOT-ALIGNED, 6/24 ACs delivered). Builds on v1.5's GA record, which is preserved. Additionally: corrected v1.5's Rollout claim that "the feature flag remains the operational control for emergency rollback" — no flag exists, so rollback needs a revert + deploy (D-12, A-8); recorded the MpInputTag reversal of D-3 (D-16, A-10, independently re-verified at ActionMultiSelectField.vue:2,25); and raised the zero-QA-coverage risk on this Lane-B, money-critical initiative (A-9). (1) Ratified RFC Decision 3.7: custom fields are org-level (team_id), not pipeline-scoped — D-1 amended, D-2 withdrawn, [QACF-S04] withdrawn (close its Jira Story Won't Do), S01/AC-4 + NEG-2 + NEG-3 replaced, Non-Goals 5–6 rewritten, CHG-001 + the flow, mermaid, constraints and telemetry properties de-pipelined. Closes RFC open question A-4. (2) Recorded three capabilities that did not ship, with owners rather than silent rewrites: array-typed fields (D-11 → A-5; was a Must Have), the ai_agent_action_custom_fields flag (D-12 → A-8; RFC Chunk 1 / BOT-4668 Won't Fix), and all custom-field telemetry (D-13 → A-7, which makes every Section 12 metric unreportable). (3) Corrected the payload contract to the as-built additional_fields[] array keyed by CRM field id (D-14) and the narrowed type whitelist (Section 7.1). (4) Surfaced that the BE merge covers all four write actions, so the create-only boundary is now a node_registries seed guarantee, not a code guarantee (D-15 → A-6). (5) Status → SHIPPED (partial); all code refs re-verified against chatbot@master + chatbot-fe@main. |
| 1.5 | 2026-07-27 | Dimas Fauzi Hidayat | Frontmatter, Header, Rollout | SHIPPED | CRM Custom Fields rolled out to General Availability — all clients/users as of 2026-07-27. PRD lifecycle draft → shipped; canonical Epic corrected to BOT-4662; rollout recorded against BE 1.324.0 and FE 3.8.3. Capture note: documentations/2026-07-27-crm-custom-fields-general-availability.md. (v1.6 note: this version recorded the release but did not amend the contract — its Rollout flag-rollback claim is corrected in v1.6.) |
| 1.4 | 2026-07-13 | Dimas Fauzi Hidayat | 15, 16 | RESOLVED | PM confirmed all three pending items: R-1 mitigation accepted (D-8), R-2 no-cap confirmed (D-9), A-3 update-actions-is-separate-PRD confirmed (D-10). Dated every row in Decisions Made (14a.2) and Alternatives Rejected (14b.3); added 2 new Alternatives Rejected entries (cap on AI cardinality; scope-creep into update actions). A-1 (CRM API contract) remains the sole open item before the RFC. |
| 1.3 | 2026-07-13 | Dimas Fauzi Hidayat | Header, intro | ADDED | Wireframes committed in-repo (documentations/crm-actions-custom-fields-wireframes.{md,html}); PRD links updated to the stable in-repo design reference. |
| 1.2 | 2026-07-13 | Dimas Fauzi Hidayat | 7.1 (new), 9.2 (S01/AC-5), 11, 15 (D-7) | ADDED | Type→component normalization contract: CRM custom-field types map to descriptors the existing ActionIntegrationForm.vue renderer already handles (one BE normalizer table, no per-type FE code); unknown types excluded from picker + ai_agent_action_custom_field_type_unsupported telemetry (new AC-5, new event). |
| 1.1 | 2026-07-13 | Dimas Fauzi Hidayat | 2, 6, 7, 8, 9, 14, 16 + Scope Changes | GROUNDED | Applied ground-prd findings vs chatbot BE + chatbot-fe FE: corrected stale line refs (extract_arguments_by_destination →186-208; lead lookup →enrich_body_for_deal_create 262-276; crm_http_client.rb →node_resources/…:45-77; ACTION_LIST →75-432); fixed array-control fact (input-tag is prototype-only — prod uses select multiple/ActionMultiSelectField); confirmed the custom-field definitions lookup is net-new (lookup_resources.rb) and sharpened A-1. |
| 1.0 | 2026-07-13 | Dimas Fauzi Hidayat | All | CREATED | Initial ADJUSTMENT PRD — custom fields (dynamic, pipeline-scoped, array-typed) on create deal / create ticket, grounded in the qontak-designer prototype + chatbot/chatbot-fe code. |