# AI Flows — Agent Guide

> Written for an AI agent. Read once top to bottom, then use §3 (playbook) and §5 (reference).
> Companion to the complete guide at https://docs.djcsystem.com/api/agents/api-guides.md — same token, conventions and error envelope. Call `GET /me` first.

An **AI flow** is a WhatsApp chatbot design: a persona (`system`), quick knowledge (`flowDesign`), trigger `keywords`, and an optional **follow-up sequence** — a graph of **blocks** (nodes) joined by **relations** (edges) that the Sequences Design canvas shows. This guide covers creating and editing flows and building their sequence through the API.

Base URL: `https://api2.djcsystem.com/v1`. Writes need the `ai-flows:write` scope.

---

## 0. Quick start

```text
1. GET  /me                                   → confirm `resources["ai-flows"]` is "write"
2. GET  /ai-flows                             → existing flows (id, name, status, keywords)
3. POST /ai-flows { name, system, flowDesign, keywords }
4. POST /ai-flows/{flowId}/blocks   × N       → the first one with `orderId: 0`
5. POST /ai-flows/{flowId}/relations × N-1    → connect them, naming the source handle
6. GET  /ai-flows/{flowId}/blocks?sort=orderId:asc   → verify, then report ids to the user
```

---

## 1. Data model

### Flow (`/ai-flows`)

| Field | Notes |
|---|---|
| `name` | required, ≤200 chars |
| `status` | `active` \| `inactive` (default `active`) |
| `system` | the chatbot persona / system prompt (≤20 000 chars). Shown as **System prompt** in the app. |
| `flowDesign` | free-text knowledge the bot answers from (≤50 000). Shown as **User's prompt**. |
| `keywords` | trigger words; `_default_` and `_master_` are reserved app-level triggers |
| `disableAI`, `collectDataOneAtATime`, `dataFields`, `roundRobin`, `autoAssignmentIds`, `forwardNumbers`, `forwardMessage`, `pinned`, `whoCanEdit` | optional behaviour settings |

Ownership and timestamps are server-owned; never send them.

### Block (`/ai-flows/{flowId}/blocks`)

Every block has `name`, `blockType`, `position {x,y}`, `orderId`, `status`, `message`, `time`/`timeUnit`, plus type-specific fields.

| `blockType` | What it does | Type fields |
|---|---|---|
| `sequential` | sends `message`, waits `time`/`timeUnit`, continues | — |
| `textNode` | like sequential, but can wait for a reply | `waitForReply`, `replyTime`, `replyTimeUnit` |
| `questionNode` | asks and collects `dataFields` from the customer | `dataFields[{name, description}]`, `questionAskOneAtATime`, `questionNoReply*`, `questionIrrelevantReply*` |
| `assignLeadNode` | assigns the lead to team members | `selectedMembers[]` (emails), `roundRobin`, `overrideAssignment` |
| `labelNode` | applies `labels[]` to the lead | `labels[{name,label,color,fontColor}]` |
| `stopFlowNode` | ends the sequence | — |
| `telegramNotificationNode` / `whatsappNotificationNode` | notifies the team | `whatsappNotificationPhones[]`, `whatsappNotificationAssignedUsers` |

Rules:
- **Exactly one block per flow has `orderId: 0`** — the entry point (badge **FIRST** on the canvas). Others default to `999`. A second `orderId: 0` → `409 CONFLICT` with `details.firstBlockId`.
- `blockType` is immutable after creation (`400` on PATCH). Delete and recreate instead.
- Poll, catalog and WhatsApp Business template nodes need app-side setup and are not creatable here (`400`).
- Deleting a block also deletes its relations and removes it from other blocks' outgoing lists.
- `position` is in canvas pixels. Blocks are ~330 px wide: use `x = 80 + i × 380` for a row.

### Relation (`/ai-flows/{flowId}/relations`)

`{ sourceNodeId, targetNodeId, sourceHandleId? }` → stored with the source block's type, and the source block's outgoing list is updated, exactly as the canvas does.

**`sourceHandleId` must be an output that exists on the source block type**, otherwise the canvas cannot draw the edge:

| Source `blockType` | Outputs (`sourceHandleId`) | Default when omitted |
|---|---|---|
| `sequential`, `telegramNotificationNode`, `whatsappNotificationNode` | `SEQUENTIAL` — after the message / notification | `SEQUENTIAL` |
| `questionNode` | `SEQUENTIAL` — after all data collected · `NO_REPLY` — no-reply follow-up · `IRRELEVANT` — irrelevant retries exhausted | `SEQUENTIAL` |
| `textNode` | `SEQUENCE` — immediately after sending · `REPLIED` — path after reply · `NO_REPLY` — path after timeout | `REPLIED` |
| `assignLeadNode`, `labelNode`, `stopFlowNode` | none — terminal | — |

A wrong handle → `400` with `details.allowedHandles` (name → meaning). A terminal source → `400`. Same source+target+handle twice → `409`. Self-loops → `400`.

---

## 2. Worked example — qualify, follow up, hand over

```bash
BASE=https://api2.djcsystem.com/v1; H='Authorization: Bearer djc_…'; J='Content-Type: application/json'

FLOW=$(curl -s -H "$H" -H "$J" -X POST $BASE/ai-flows -d '{
  "name": "Consultation booking", "keywords": ["consult"],
  "system": "You are Aiden, our sales consultant… never say you are an AI.",
  "flowDesign": "Business hours Mon–Fri 9–6 (Asia/Kuala_Lumpur)…" }' | jq -r .data.id)

B=$BASE/ai-flows/$FLOW/blocks; R=$BASE/ai-flows/$FLOW/relations
WELCOME=$(curl -s -H "$H" -H "$J" -X POST $B -d '{"name":"Welcome","blockType":"sequential","orderId":0,"position":{"x":80,"y":200},"message":"Hi {{name}}, thanks for reaching out!"}' | jq -r .data.id)
QUALIFY=$(curl -s -H "$H" -H "$J" -X POST $B -d '{"name":"Qualify","blockType":"questionNode","position":{"x":460,"y":200},"message":"What budget and timeline do you have in mind?","dataFields":[{"name":"budget"},{"name":"timeline"}],"questionAskOneAtATime":true}' | jq -r .data.id)
NUDGE=$(curl -s -H "$H" -H "$J" -X POST $B -d '{"name":"Follow-up (1 day)","blockType":"textNode","position":{"x":840,"y":200},"message":"Would Tue 11am or Thu 3pm suit you?","time":1,"timeUnit":"days","waitForReply":true,"replyTime":2,"replyTimeUnit":"days"}' | jq -r .data.id)
ASSIGN=$(curl -s -H "$H" -H "$J" -X POST $B -d '{"name":"Assign to sales","blockType":"assignLeadNode","position":{"x":1220,"y":200},"roundRobin":true}' | jq -r .data.id)
STOP=$(curl -s -H "$H" -H "$J" -X POST $B -d '{"name":"No reply — stop","blockType":"stopFlowNode","position":{"x":1220,"y":520}}' | jq -r .data.id)

curl -s -H "$H" -H "$J" -X POST $R -d "{\"sourceNodeId\":\"$WELCOME\",\"targetNodeId\":\"$QUALIFY\"}"                                   # SEQUENTIAL
curl -s -H "$H" -H "$J" -X POST $R -d "{\"sourceNodeId\":\"$QUALIFY\",\"targetNodeId\":\"$NUDGE\"}"                                     # SEQUENTIAL = data collected
curl -s -H "$H" -H "$J" -X POST $R -d "{\"sourceNodeId\":\"$NUDGE\",\"targetNodeId\":\"$ASSIGN\",\"sourceHandleId\":\"REPLIED\"}"
curl -s -H "$H" -H "$J" -X POST $R -d "{\"sourceNodeId\":\"$NUDGE\",\"targetNodeId\":\"$STOP\",\"sourceHandleId\":\"NO_REPLY\"}"
```

Result on the canvas: Welcome **FIRST** → Qualify → Follow-up, whose *Path after reply* goes to Assign and *Path after timeout* to Stop.

---

## 3. Playbook

1. **Inspect first**: `GET /ai-flows` and, for the flow you will touch, `GET …/blocks?sort=orderId:asc` and `GET …/relations`. Summarise the existing sequence before changing it.
2. **One entry point.** If a first block exists, extend from it — do not try to add another `orderId: 0`.
3. **Blocks before relations.** Create all blocks, keep their ids, then connect. Choose the handle deliberately: `REPLIED` vs `NO_REPLY` decides the customer's experience.
4. **Every non-terminal branch should end somewhere** — an `assignLeadNode`, `labelNode` or `stopFlowNode`. Leaving `NO_REPLY` unconnected is allowed but usually a gap worth mentioning to the user.
5. **Edit in place** with `PATCH` for copy changes; recreate only when the `blockType` must change.
6. **Lay out for humans**: left-to-right in execution order, branches on a second row (`y + 320`).
7. **Report** the flow id, each block id with its name and type, and the relations you created.

---

## 4. Errors you will meet

| Status | `code` | Typical cause |
|---|---|---|
| 400 | `BAD_REQUEST` | unknown field, unsupported `blockType`, wrong `sourceHandleId` (`details.allowedHandles`), terminal source, self-loop, PATCHing `blockType` |
| 403 | `INSUFFICIENT_SCOPE` | token has `ai-flows:read` only — writes need `ai-flows:write` |
| 404 | `NOT_FOUND` | flow not in the token's workspace, or a block id that is not in this flow |
| 409 | `CONFLICT` | second first-block, duplicate relation |

---

## 5. Reference

| Method | Path | Scope |
|---|---|---|
| GET | `/ai-flows` · `/ai-flows/{id}` | read |
| POST · PATCH · DELETE | `/ai-flows` · `/ai-flows/{id}` | write |
| GET | `/ai-flows/{id}/blocks` (`filter[blockType]`, `filter[orderId]`, `sort=orderId:asc`) · `/blocks/{blockId}` | read |
| POST · PATCH · DELETE | `/ai-flows/{id}/blocks[/{blockId}]` | write |
| GET | `/ai-flows/{id}/relations` (`filter[sourceNodeId]`, `filter[targetNodeId]`) · `/relations/{relId}` | read |
| POST · DELETE | `/ai-flows/{id}/relations[/{relId}]` | write |

The live, always-current field list is `GET /resources` → the `ai-flows` entry and its `children`.
