# Generate WhatsApp Flow Generate a config-driven WhatsApp conversation flow JSON for the Helix Engage flow runtime engine. ## When to use When the user asks to create a new WhatsApp flow, chatbot flow, or conversation automation — e.g., "create a WhatsApp flow for prescription refills", "build a feedback collection flow", "add a lab report flow". ## Flow Runtime Architecture The flow engine reads JSON flow definitions from `src/messaging/flow/default-flows/` and executes them at runtime. Each flow is a graph of **Groups** (containers) containing **Blocks** (steps), connected by **Edges**. ### Execution Model ``` Inbound WhatsApp message → match flow by trigger → create/resume session → walk forward through Groups → Blocks: MessageBlock → send text/buttons/list to patient InputBlock → PAUSE, wait for next message ConditionBlock → evaluate variable, follow matching edge SetVariableBlock → assign/transform variable ToolCallBlock → call registered tool AIBlock → generate LLM response JumpBlock → jump to another group → End of group → follow outgoing edge → next group → No more edges → flow complete, session cleared ``` Session state stored in Redis with 24h TTL. Per-phone execution lock prevents concurrent flows. ### Flow JSON Schema ```typescript type Flow = { id: string; // "flow-{kebab-name}" name: string; // Human-readable name description: string; // Admin-facing description trigger: FlowTrigger; // What starts this flow groups: Group[]; // Ordered containers of blocks edges: Edge[]; // Connections between blocks/groups variables: VariableDefinition[];// Flow-scoped variables version: number; // Start at 1 status: 'draft' | 'published'; // Only published flows execute }; type FlowTrigger = | { type: 'message'; conditions?: { keywords?: string[]; regex?: string } } | { type: 'default' }; // Catch-all when no other flow matches type Group = { id: string; // "g1", "g2", etc. title: string; // "Greeting", "Department Selection" blocks: Block[]; // Executed in order }; type Edge = { id: string; // "e1", "e2", etc. from: { blockId: string; conditionId?: string }; to: { groupId: string; blockId?: string }; }; type VariableDefinition = { id: string; // "v1", "v2", etc. name: string; // "selectedDepartment" type: 'string' | 'number' | 'boolean' | 'object' | 'array'; defaultValue?: any; }; ``` ### Block Types ```typescript // Send text, buttons, or list to patient type MessageBlock = { id: string; type: 'message'; content: | { format: 'text'; text: string } // Supports {{variables}} | { format: 'buttons'; text: string; buttons: { id: string; title: string }[] } // Max 3 buttons, title max 20 chars | { format: 'list'; text: string; buttonText: string; sections: { title: string; rows: { id: string; title: string; description?: string }[] }[] }; // Section title max 24 chars, row title max 24 chars, max 10 rows total }; // Wait for patient reply — PAUSES execution type InputBlock = { id: string; type: 'input'; inputType: 'text' | 'interactive_reply' | 'any'; variableId: string; // Store reply in this variable validation?: { regex?: string; errorMessage?: string }; }; // Branch based on variable value type ConditionBlock = { id: string; type: 'condition'; conditions: { id: string; // "c1" — used in edge.from.conditionId variableId: string; operator: 'equals' | 'contains' | 'exists' | 'not_exists' | 'gt' | 'lt' | 'starts_with'; value?: string; // Supports {{variables}} }[]; }; // Assign or transform a variable type SetVariableBlock = { id: string; type: 'set_variable'; variableId: string; value: string; expression?: 'extract_id' | 'extract_datetime' | 'date_tomorrow' | 'date_day_after'; // extract_id: "doc:{uuid}:{name}" → uuid (second segment) // extract_datetime: "slot:{id}:{datetime}" → datetime (third+ segments, rejoined with :) // date_tomorrow/date_day_after: computes date string YYYY-MM-DD }; // Execute a registered tool type ToolCallBlock = { id: string; type: 'tool_call'; toolName: string; // Must be a registered tool (see below) inputs: Record; // Values support {{variables}} and {{var.field}} dot notation outputVariableId?: string; }; // Generate dynamic LLM response type AIBlock = { id: string; type: 'ai'; prompt: string; // Supports {{variables}} outputVariableId?: string; sendToPatient: boolean; // true = send as WhatsApp message }; // Jump to another group type JumpBlock = { id: string; type: 'jump'; targetGroupId: string; }; ``` ### Available Tools (ToolRegistry) | Tool Name | Description | Inputs | Output | |---|---|---|---| | `resolve_caller` | Phone → Lead + Patient | phone? (defaults to current) | { leadId, patientId, isNew, phone } | | `send_department_list` | Interactive department list | (none) | { sent, departments[] } | | `send_doctor_list` | Interactive doctor list | department | { sent, count } | | `send_slot_list` | Time slots for doctor+date | doctorId, doctorName, date | { sent, slots } | | `send_confirm_buttons` | Confirm/Cancel buttons | summary | { sent } | | `book_appointment` | Book with conflict check | patientName, phoneNumber, department, doctorName, scheduledAt, reason | { booked, appointmentId, reference } | | `lookup_appointments` | Check existing appointments | (none — uses current caller) | { appointments[] } | | `send_appointment_qr` | Generate and send QR code | appointmentId, reference, patientName, doctorName, department, scheduledAt | { sent, qrUrl } | ### System Variables (auto-injected) | Variable | Description | |---|---| | `_initialMessage` | The first message the patient sent | | `_senderName` | WhatsApp profile name | | `_phone` | Phone number (E.164 without +) | | `_callerName` | Resolved patient name from platform | | `_leadId` | Lead ID if exists | | `_patientId` | Patient ID if exists | | `_isNew` | true if no prior records | ### Variable Interpolation - `{{variableName}}` — simple substitution - `{{result.fieldName}}` — dot notation for object fields (e.g., `{{bookingResult.appointmentId}}`) - Interactive reply IDs stored in `variableId`, display titles in `variableId_title` ### WhatsApp Constraints - Button title: max 20 characters - List section title: max 24 characters - List row title: max 24 characters - List row description: max 72 characters - Max 3 buttons per message - Max 10 list rows total across all sections - No markdown in text messages (plain text only) - Interactive messages only work within 24h session window ## How to Generate 1. **Ask the user** what the flow should do — purpose, steps, what data to collect 2. **Design the groups** — each logical phase is a group (Greeting, Selection, Confirmation, etc.) 3. **Define variables** — what data flows through the conversation 4. **Build blocks** — MessageBlocks for output, InputBlocks to pause for reply, ConditionBlocks for branching, ToolCallBlocks for platform operations, AIBlocks for dynamic responses 5. **Wire edges** — connect groups via edges, condition edges for branching 6. **Write the JSON** to `src/messaging/flow/default-flows/{flow-name}.json` 7. **Register new tools** if needed in `src/messaging/flow/tool-registry.ts` ## Reference See `src/messaging/flow/default-flows/appointment-booking.json` for a complete working example with: - AI greeting - Intent routing (book / check / question) - Interactive lists (departments, doctors, slots) - Date selection with custom date AI parsing - Confirmation buttons - Booking with conflict check - QR code generation ## Deployment After creating the flow JSON: 1. `npm run build` — verifies the JSON is copied to dist (via nest-cli.json assets) 2. Deploy to EC2 — the flow store auto-seeds on first run if `data/flows/` is empty 3. If updating an existing flow: `docker exec sidecar cp /app/dist/.../flow.json /app/data/flows/flow-id.json && docker compose restart sidecar`