Files
helix-engage-server/.claude/skills/generate-whatsapp-flow.md
saridsa2 bbea12185d
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: Claude skill for generating WhatsApp flow JSON definitions
Skill documents the full flow schema (Groups, Blocks, Edges, Variables),
all available tools, WhatsApp constraints, system variables, and
deployment steps. Enables generating new flows from natural language
descriptions — e.g., "create a prescription refill flow".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 21:31:00 +05:30

8.3 KiB

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

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

// 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<string, string>; // 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