feat: Claude skill for generating WhatsApp flow JSON definitions
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

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>
This commit is contained in:
2026-04-20 21:31:00 +05:30
parent f1c026cf7a
commit bbea12185d

View File

@@ -0,0 +1,203 @@
# 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<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`