# Flows

A **Flow** is a conversation graph that belongs to a single [Agent](/docs/concepts/agents). It defines how the agent reacts to user input: which questions to ask, which AI behavior to run, which external API to call, which conditions to branch on.

Flows are *agent-scoped*. They are not reusable across agents — clone or recreate them if you need a similar shape elsewhere.

## Identity

| Field | Type | Notes |
|  --- | --- | --- |
| `id` | number | Use as `flowId` in nested routes. |
| `name` | string | Display name. |
| `description` | string? |  |
| `status` | enum | `ACTIVE` · `DRAFT` · `PAUSED` · `ARCHIVED`. |
| `runCount` | number | Total executions. |
| `assistantId` | string | Owning agent (always returned in detail). |
| `isDefault` | boolean | One flow per agent is the default entry point. |
| `liveSnapshotId` | string? | MongoDB ObjectId of the published snapshot. |
| `draftSnapshotId` | string? | ObjectId of the working draft. |
| `createdAt` | string | ISO timestamp. |
| `updatedAt` | string | ISO timestamp. |


## The graph

A flow's behavior lives in its **graph**: a set of **nodes** connected by **edges**.

### Node types

| Type | Purpose |
|  --- | --- |
| `START` | Entry point. Every flow has exactly one. |
| `TRIGGER_INTENT` | Branches the conversation when an Intent matches. |
| `SAY_AI` | Sends a message. Optionally generated by the AI model. |
| `RESPONSE_AI` | Captures the user's free-text answer guided by `instructions`. |
| `TOOLS_AI` | Lets the AI decide which custom Tool to call (incl. table tools). |
| `API` | Calls an external HTTP endpoint and stores the result. |
| `CONDITIONAL_ROUTING` | Branches based on AI-evaluated conditions over the context. |
| `CREATE_RECORD_ACTIVITY` | Logs a manual activity (note, call, meeting, email, WhatsApp) on a specific Object record. |


> The `DYNAMIC_TABLES` ("Data Action") node is **automation-only** and is not available in flows. To read/write records inside a flow, use a `TOOLS_AI` node with table tools.


Each node has a stable `nodeId` (regex `[a-zA-Z0-9_-]+_\d+`), an `alias`, a `position` for the canvas, and a `data` payload whose shape depends on `type`.

### Edges

Edges connect a node's `handle` (output port) to another node's `targetEdge` (input). The same edge object lives both inline on the node and as a global list — the API normalizes both on write.

> **Reference validation**: on create/update, the API validates every cross-resource reference inside a node's `data` payload — `intentIds` (TRIGGER_INTENT), `customToolIds` / `playbookIds` / `selectedRecordTypes[].id` (TOOLS_AI), `assistantId` (RESPONSE_AI), `aiModelId`, `knowledgeBaseIds`, etc. If any referenced id does not exist or does not belong to your account, the request returns `400 bad_request` with `details.issues[].code === "not_found"` and **no partial state is persisted**. See [Errors](/docs/errors#example-node-references).


### Snapshots

When you publish a flow, the **draft snapshot** is promoted to the **live snapshot**. The agent serves traffic from `liveSnapshotId`. Mutations via `POST /nodes`, `PUT /nodes/{nodeId}`, `POST /edges`, etc. write to the draft.

## The Start flow & triggering

- **Default Start flow.** Every agent ships with a default flow named **"Start"** (in `DRAFT`) containing a single `START` node. It runs **automatically at the beginning of every conversation** — first-contact logic (greeting, capturing data, creating a record) belongs here, added after the `START` node. It's the flow with `isDefault: true`.
- **The `START` node is unique.** Only the default Start flow may contain a `START` node; the API rejects a `START` node in any other flow with `400`.
- **Other flows are triggered.** A non-default flow begins with a `TRIGGER_INTENT` node and is entered when it matches the user, configured by either:
  - **Intents** — `intentIds` on the `TRIGGER_INTENT` node; the flow fires when the user's message matches one of them.
  - **Agentic routing** — `agenticRouting`, a natural-language instruction describing when to enter the flow; the agent routes based on it and the user's intention.
- **Default-message rule.** If a flow finishes a turn without producing a user-facing message (e.g. it only captured variables or ran a Dynamic Tables action), the agent sends a **default AI-generated message** so the user always receives a reply. This applies to every flow.


## Variable interpolation

Text fields can reference variables as `{VARIABLE_NAME}`. The name must match exactly (case-sensitive). Interpolable fields include:

- API node: `url`, `headers[].value`, `parameters[].value`, `body`.
- Say AI: `message`, `prompt`.
- Response AI: `instructions`.
- Tools AI: `instructions`.
- Conditional Routing: `conditions[].expression`.
- Dynamic Tables: `rowId`, `search`, `rowData` values.
- Create Record Activity: `rowId`, `content`.


See [Flow Variables](/docs/concepts/flow-variables).

## Capturing values into variables

Interpolation *reads* a variable; capturing *writes* one. The field that populates a variable depends on the node type:

| Node | Field | Item shape |
|  --- | --- | --- |
| Tools AI / Response AI / AI Capture | `captureVariables` | reference an existing variable |
| API | `variables` | `{ key, value, fullResponse }` — `key` names an existing variable, `value` is a path into the JSON response |


**Capture targets must reference a variable that already exists.** Create it first (`POST /agents/{agentId}/variables` for flows, `POST /workflows/{workflowId}/variables` for automations). In `captureVariables` you may pass just `{ "name": "<var>" }` (or `{ "id": <id> }`) — the API resolves it and stores the canonical `{ id, name, description }` so it is linked and rendered in the app. An unknown name/id (or an API `variables[].key` that doesn't exist) returns `400`.

Captured values are matched by **name**, so an AI node and an API node can write to the same `{name}`. The two node families use **different fields** (`captureVariables` vs `variables`); the wrong field on a node is silently ignored.

### API node response paths (`value`)

For an API node, `value` is a **property path** into the parsed JSON response — simple property access, **not** JSONPath (no `$`, wildcards, or filters):

- Dot notation for nested objects: `data.user.email`.
- `[n]` for array indices: `data.items[0].id` (`[0]` is equivalent to `.0`; a leading dot is optional).
- Empty `value`, or `fullResponse: true`, captures the **whole** response body.


For the response `{"data":{"items":[{"id":42,"price":9.99}],"total":1}}`:

| `value` | Captured |
|  --- | --- |
| `data.total` | `1` |
| `data.items[0].id` | `42` |
| `data.items[0].price` | `9.99` |


Path extraction only applies when the response body is JSON. If a step in the path is missing or `null`, the variable is left unset (no error). Captured objects/arrays are stored as a JSON string; primitives as their string value.

### `CREATE_RECORD_ACTIVITY` node

Logs a manual activity on a specific Object record during a conversation flow.


```json
{
    "nodeId": "node_create_activity_1",
    "type": "CREATE_RECORD_ACTIVITY",
    "position": { "positionX": 640, "positionY": 0 },
    "data": {
        "type": "CREATE_RECORD_ACTIVITY",
        "recordTypeId": 5,
        "rowId": "{contact_row_id}",
        "activityType": "NOTE",
        "content": "El usuario preguntó sobre: {user_question}"
    }
}
```

| Field | Required | Notes |
|  --- | --- | --- |
| `recordTypeId` | ✅ | Numeric id of the Object's record type. Validated: must exist in your account. |
| `rowId` | ✅ | MongoDB ObjectId of the record. Supports `{varName}` interpolation. |
| `activityType` | ✅ | `NOTE`, `EMAIL`, `PHONE_CALL`, `MEETING`, or `WHATSAPP`. Validated at save time. |
| `content` | ✅ | Activity body text. Supports `{varName}` interpolation. |


Variables in `rowId` and `content` must exist — the API returns `400` if any `{varName}` is unknown.

## Operations

| Verb | Path | Purpose |
|  --- | --- | --- |
| `GET` | `/public/v1/agents/{agentId}/flows` | List flows |
| `POST` | `/public/v1/agents/{agentId}/flows` | Create |
| `GET` | `/public/v1/agents/{agentId}/flows/{flowId}` | Detail (`?includeNodes=true` for nodes) |
| `PUT` | `/public/v1/agents/{agentId}/flows/{flowId}` | Update metadata / status |
| `DELETE` | `/public/v1/agents/{agentId}/flows/{flowId}` | Soft delete |
| `GET` | `/public/v1/agents/{agentId}/flows/{flowId}/graph` | Full graph (nodes + edges) |
| `POST` | `/public/v1/agents/{agentId}/flows/{flowId}/nodes` | Create node |
| `PUT` | `/public/v1/agents/{agentId}/flows/{flowId}/nodes/{nodeId}` | Update node |
| `DELETE` | `/public/v1/agents/{agentId}/flows/{flowId}/nodes/{nodeId}` | Delete node + incident edges |
| `POST` | `/public/v1/agents/{agentId}/flows/{flowId}/edges` | Add/replace edge |
| `DELETE` | `/public/v1/agents/{agentId}/flows/{flowId}/edges` | Remove edge |


## CLI


```bash
frontline agents flows list --table
frontline agents flows create --name "Order Routing"
frontline agents flows nodes create --data '{"nodeId":"start_1","type":"START","position":{"positionX":0,"positionY":0}}'
frontline agents flows edges add --source start_1 --source-handle default --target say_1 --target-handle default
```