# Flows: guide a call step by step

> A flow drives a call through steps instead of instructions alone: an AI conversation, a keypad menu (IVR) or a script for human agents. Build it in the editor, publish it, and attach it to an assistant, a route or a call.

An [assistant](https://docs.morevoice.ai/guides/assistants/) follows its instructions. A **flow** gives a call structure: it walks the call through steps, such as greet, ask for the customer's ID, look the order up on your server, answer, offer a transfer, say goodbye, and branches on what happens at each one. Use a flow when the conversation has to follow your process every time, and the instructions alone when the call can wander.

Flows are built in the visual editor, **Build › Flows**: on a canvas, from a template, or by describing the flow in words and letting the AI draw it. There are three kinds:

| Kind | What it does |
| --- | --- |
| **Voice AI flow** | Guides an AI assistant through a phone call, step by step. The assistant still talks naturally inside each step. |
| **IVR** | A keypad and speech menu for inbound lines ("press 1 for sales"), with opening hours, Shabbat and holidays. No AI model is needed. |
| **Agent script** | A branching script the [copilot](https://docs.morevoice.ai/get-started/what-is-morevoice/#the-building-blocks) shows to human agents during their calls. |

## How a flow runs

1. **Before the call**, a *Pre-call API* node can fetch the caller's details from your CRM, so the greeting can use them.

2. **The call starts at *Start*** and moves from node to node. Each node has transitions, and the first one that matches moves the call on: what the caller said (decided by the model), a key they pressed, a rule on the variables, or a time-out.

3. **Global nodes** can interrupt from anywhere: a *Knowledge answer* for a question off the script, a *Transfer call* when the caller asks for a person. *Return* goes back to where the call was.

4. **After the call**, the nodes of the *After the call* phase run on their own: extract details, classify the call, set its outcome, and send the results to your systems.

## Nodes

### Before the call

| Node | Type | What it does | In flows for |
| --- | --- | --- | --- |
| **Pre-call API** | `precall_api` | Fetch the caller's details before saying hello (CRM lookup). | Voice AI, IVR |

### During the call

| Node | Type | What it does | In flows for |
| --- | --- | --- | --- |
| **Start** | `start` | Where every call begins — the greeting. | Voice AI, Agent script, IVR |
| **Say** | `say` | Say an exact line (instant, from cache) or a line the AI rephrases naturally. | Voice AI, IVR |
| **Conversation** | `dialogue` | A free conversation step with a goal — the AI talks until a transition matches. | Voice AI |
| **Ask & collect** | `question` | Ask for one detail (name, date, ID…), validate it and store it in a variable. | Voice AI, IVR |
| **Decision** | `decision` | Branch by rules on variables — no AI, always the same result. | Voice AI, Agent script, IVR |
| **Knowledge answer** | `kb_answer` | Answer the caller's question from your knowledge base. | Voice AI |
| **API / Webhook** | `api` | Call any system (CRM, calendar, your server) and use the answer in the call. | Voice AI, IVR |
| **Set variable** | `set_var` | Store or calculate a value for later steps. | Voice AI, Agent script, IVR |
| **SMS** | `sms` | Send a text message during the call (link, confirmation, address). | Voice AI, IVR |
| **Email** | `email` | Send an email during the call. | Voice AI, IVR |
| **Transfer call** | `transfer` | Hand the caller to your agents, another number, or another AI agent. | Voice AI, Agent script, IVR |
| **Add participant** | `add_participant` | Dial someone into the call — the AI greets them and runs a three-way call. | Voice AI |
| **Keypad menu** | `dtmf_menu` | "Press 1 for… 2 for…" — the caller may also just say the option. | Voice AI, IVR |
| **Payment pause** | `pci_pause` | Stop recording and transcribing while the caller pays (card details never reach the AI); resume after. | Voice AI, IVR |
| **Business hours** | `hours` | Open, closed or a holiday right now? Weekly hours, Shabbat and Israeli holidays, special dates. | IVR, Voice AI |
| **Switch language** | `set_language` | Continue the call in another language and voice. | IVR, Voice AI |
| **Voicemail** | `voicemail` | The caller leaves a message after the beep — recorded, transcribed and summarized. | IVR, Voice AI |
| **Schedule callback** | `schedule_callback` | Book a time to call the customer back. | Voice AI, IVR |
| **End call** | `end` | Say goodbye, save the outcome and hang up. | Voice AI, IVR |
| **Return** | `return` | Go back to where the caller was before this side-topic. | Voice AI, Agent script, IVR |
| **CRM / calendar action** | `integration` | Use a connected CRM or calendar during the call: look the caller up, add a note, create a task. | Voice AI, IVR |

### After the call

| Node | Type | What it does | In flows for |
| --- | --- | --- | --- |
| **After the call** | `post_start` | Everything here runs automatically after the call ends. | Voice AI, IVR |
| **Extract details** | `extract` | Let AI pull structured details out of the conversation. | Voice AI, IVR |
| **Classify call** | `classify` | Let AI pick one category for the call and branch on it. | Voice AI, IVR |
| **Set outcome** | `set_outcome` | Save the call's result (disposition) for reports and campaigns. | Voice AI, IVR |
| **API / Webhook** | `post_api` | Send the call's results to any system after it ends. | Voice AI, IVR |
| **CRM / calendar action** | `post_integration` | After the call, update a connected CRM: add a note, create a follow-up task. | Voice AI, IVR |
| **SMS** | `post_sms` | Text the customer after the call (summary, link, confirmation). | Voice AI, IVR |
| **Email** | `post_email` | Email the summary to your team or the customer. | Voice AI, IVR |

### Agent scripts

| Node | Type | What it does | In flows for |
| --- | --- | --- | --- |
| **Call step** | `step` | One step of the agent's script: what to say, what to ask, what not to miss. | Agent script |
| **Customer answer** | `branch` | Branch the script by what the customer answers. | Agent script |
| **Objection** | `objection` | When the customer objects anywhere in the call — how to answer, then go back. | Agent script |
| **Call outcome** | `outcome` | How the call ended (sale, follow-up, not interested…) and what to wrap up. | Agent script |

## Variables

Text in a flow can use `{{variables}}`: `שלום {{customer_name}}`, or `{{call.from}}` in the body of an API request.

- **The call's variables**: what you pass as `variables` when you [create a call](https://docs.morevoice.ai/guides/outbound-calls/#variables), and a campaign contact's columns.
- **System variables**: `{{call.id}}`, `{{call.from}}`, `{{call.to}}`, `{{call.direction}}`; `{{now.date}}`, `{{now.time}}` and `{{now.weekday}}` in Israel time; `{{contact.…}}` and `{{campaign.…}}` on campaign calls.
- **Variables the flow sets**: *Ask & collect* stores the answer it validated, *Set variable* stores or calculates a value, and an *API / Webhook* node maps fields of your server's answer to variables.
- **Secrets**: `{{secret.NAME}}` puts a stored secret (an API token, say) into an API request. Its value never appears in the editor, the logs or the API.

## Publish and versions

The editor saves a **draft** as you work. **Publishing** checks the flow first: errors (a step with no way out, an API node without a URL) block it, warnings don't. Each publish creates a numbered **version**, and only the published version runs on calls, so you can keep editing the draft safely. Any earlier version can be restored into the draft and published again. Every publish sends the [`flow.published`](https://docs.morevoice.ai/webhooks/events/#flow.published) event.

A flow an assistant or a copilot profile uses can't be deleted: detach it first.

## Attach a flow

A published voice flow drives every call of the assistant it is attached to (`flow_id` on the assistant), or one outbound call (`flow_id` when you create the call, instead of `assistant_id`):

**cURL**

```sh
# Let a published voice flow drive the assistant's calls, step by step. null detaches it.
curl -X PATCH https://api.morevoice.ai/v1/assistants/$ASSISTANT_ID \
  -H "Authorization: Bearer $MOREVOICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "flow_id": "'"$FLOW_ID"'" }'

# Or run the flow for one outbound call only: it runs on its linked assistant.
curl https://api.morevoice.ai/v1/calls \
  -H "Authorization: Bearer $MOREVOICE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "to": "+972500000001",
    "flow_id": "'"$FLOW_ID"'",
    "purpose": "service",
    "variables": { "customer_name": "דנה" }
  }'
```

**Node.js**

```ts title="attach-flow.ts"
import MoreVoice from "@morevoice/sdk";

const mv = new MoreVoice(); // reads MOREVOICE_API_KEY
const flowId = process.env.FLOW_ID!;

// Let a published voice flow drive the assistant's calls, step by step. null detaches it.
await mv.assistants.update(process.env.ASSISTANT_ID!, { flow_id: flowId });

// Or run the flow for one outbound call only: it runs on its linked assistant.
const call = await mv.calls.create({
	to: "+972500000001",
	flow_id: flowId,
	purpose: "service",
	variables: { customer_name: "דנה" },
});
console.log(call.id);
```

**Python**

```python title="attach_flow.py"
import os
import uuid

import requests

API = "https://api.morevoice.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}
flow_id = os.environ["FLOW_ID"]

# Let a published voice flow drive the assistant's calls, step by step. None detaches it.
response = requests.patch(f"{API}/assistants/{os.environ['ASSISTANT_ID']}", headers=HEADERS, json={"flow_id": flow_id}, timeout=30)
response.raise_for_status()

# Or run the flow for one outbound call only: it runs on its linked assistant.
response = requests.post(
    f"{API}/calls",
    headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
    json={"to": "+972500000001", "flow_id": flow_id, "purpose": "service", "variables": {"customer_name": "דנה"}},
    timeout=30,
)
response.raise_for_status()
print(response.json()["id"])
```

An IVR or voice flow can also answer a number directly: give an [inbound route](https://docs.morevoice.ai/guides/inbound-calls/#targets) a `flow` target.

## Follow a call through its flow

`GET /v1/calls/{id}/flow_path` shows the nodes a call went through: when it entered each one, how the step was chosen (`llm`, `dtmf`, `condition`, `timeout`), how many turns the caller spent in it, and how the flow ended. While the call runs, `live` is `true` and more steps follow.

**cURL**

```sh
# The nodes a call went through in its flow: how each step was chosen, when, and how the flow ended.
curl https://api.morevoice.ai/v1/calls/$CALL_ID/flow_path \
  -H "Authorization: Bearer $MOREVOICE_API_KEY"
```

**Node.js**

```ts title="flow-path.ts"
import MoreVoice from "@morevoice/sdk";

const mv = new MoreVoice(); // reads MOREVOICE_API_KEY

// The nodes a call went through in its flow: how each step was chosen, when, and how the flow ended.
const path = await mv.calls.retrieveFlowPath(process.env.CALL_ID!);
for (const step of path.steps) {
	console.log(`${(step.at_ms / 1000).toFixed(1)}s`, step.node_id, step.via ?? "start");
}
console.log(`flow ${path.flow_id} v${path.flow_version} ended at ${path.end}${path.live ? " (still running)" : ""}`);
```

**Python**

```python title="flow_path.py"
import os

import requests

API = "https://api.morevoice.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}

# The nodes a call went through in its flow: how each step was chosen, when, and how the flow ended.
response = requests.get(f"{API}/calls/{os.environ['CALL_ID']}/flow_path", headers=HEADERS, timeout=30)
response.raise_for_status()
path = response.json()
for step in path["steps"]:
    print(f"{step['at_ms'] / 1000:.1f}s", step["node_id"], step["via"] or "start")
running = " (still running)" if path["live"] else ""
print(f"flow {path['flow_id']} v{path['flow_version']} ended at {path['end']}{running}")
```

When a node fails during a call (your API timed out, an SMS couldn't be sent), MoreVoice sends the [`flow.node_failed`](https://docs.morevoice.ai/webhooks/events/#flow.node_failed) event, and the call follows the node's error transition.

> **Tip:** Test a flow before you publish it: the editor's simulator runs it as a chat, or calls you on the phone, and shows the path it took. See [simulation and test calls](https://help.morevoice.ai/en/flows/simulate-and-test/) in the help centre.

## The flows API

`/v1/flows` builds and publishes flows from code, so a flow can live in your repository and ship with your release:

| Request | What it does |
| --- | --- |
| `POST /v1/flows` | Creates a flow (`voice`, `ivr` or `agent_script`) with an empty draft, a template's, or the `graph` you send. |
| `GET` / `PUT /v1/flows/{id}/draft` | Reads and replaces the editable draft. `base_rev` is the draft's `rev` you read: a draft changed since answers `409` and nothing is written. |
| `POST /v1/flows/validate` | Checks a graph without saving it: the errors that would block publishing, and warnings. |
| `POST /v1/flows/{id}/publish` | Publishes the draft as a new version, with a `note`; calls use it at once. A draft with errors answers `409 flow_invalid`, with what to fix. |
| `GET /v1/flows/{id}/versions`, `…/restore` | Lists the published versions, and copies one back into the draft. |
| `POST /v1/flows/{id}/duplicate`, `GET …/usage` | Copies a flow, and shows what uses it (assistants and copilot profiles): a flow in use can't be deleted. |

**cURL**

```sh
# Change the greeting of a flow's draft, then publish it. base_rev is the draft revision you read:
# if someone changed the draft meanwhile, the write answers 409 and nothing is saved.
draft=$(curl -s https://api.morevoice.ai/v1/flows/$FLOW_ID/draft -H "Authorization: Bearer $MOREVOICE_API_KEY")

echo "$draft" \
  | jq '{ base_rev: .rev, graph: (.graph | (.nodes[] | select(.type == "start") | .start.greeting) |= "שלום, הגעתם לאקמה. במה אפשר לעזור?") }' \
  | curl -X PUT https://api.morevoice.ai/v1/flows/$FLOW_ID/draft \
      -H "Authorization: Bearer $MOREVOICE_API_KEY" \
      -H "Content-Type: application/json" \
      --data-binary @-

curl https://api.morevoice.ai/v1/flows/$FLOW_ID/publish \
  -H "Authorization: Bearer $MOREVOICE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "note": "New greeting" }'
```

**Node.js**

```ts title="edit-and-publish.ts"
import MoreVoice from "@morevoice/sdk";

const mv = new MoreVoice(); // reads MOREVOICE_API_KEY
const flowId = process.env.FLOW_ID!;

// Change the greeting of a flow's draft, then publish it. base_rev is the draft revision you read:
// if someone changed the draft meanwhile, the write answers 409 and nothing is saved.
const draft = await mv.flows.retrieveDraft(flowId);
for (const node of draft.graph.nodes) {
	if (node.type === "start" && node.start) node.start.greeting = "שלום, הגעתם לאקמה. במה אפשר לעזור?";
}
await mv.flows.updateDraft(flowId, { base_rev: draft.rev, graph: draft.graph });

const flow = await mv.flows.publish(flowId, { note: "New greeting" });
console.log(`${flow.name}: version ${flow.published_version} is live`);
```

**Python**

```python title="edit_and_publish.py"
import os

import requests

API = "https://api.morevoice.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}
flow_id = os.environ["FLOW_ID"]

# Change the greeting of a flow's draft, then publish it. base_rev is the draft revision you read:
# if someone changed the draft meanwhile, the write answers 409 and nothing is saved.
response = requests.get(f"{API}/flows/{flow_id}/draft", headers=HEADERS, timeout=30)
response.raise_for_status()
draft = response.json()
for node in draft["graph"]["nodes"]:
    if node["type"] == "start" and node.get("start"):
        node["start"]["greeting"] = "שלום, הגעתם לאקמה. במה אפשר לעזור?"

response = requests.put(
    f"{API}/flows/{flow_id}/draft",
    headers=HEADERS,
    json={"base_rev": draft["rev"], "graph": draft["graph"]},
    timeout=30,
)
response.raise_for_status()

response = requests.post(f"{API}/flows/{flow_id}/publish", headers=HEADERS, json={"note": "New greeting"}, timeout=30)
response.raise_for_status()
flow = response.json()
print(f"{flow['name']}: version {flow['published_version']} is live")
```

The API's nodes are a stable public subset of the editor's: `start`, `say`, `question`, `ai_agent`, `kb_answer`, `api`, `post_api`, `integration`, `post_integration`, `set_variable`, `decision`, `dtmf_menu`, `hours`, `transfer` and `end`. Every other node (an SMS, a voicemail, an agent-script step, and any node the editor adds later) reads as `{ "type": "internal" }` and is written back exactly as it was, so a new node in the editor never breaks your code.

> **Coming soon: Simulation and analytics over the API**
>
> Running a flow as a test conversation (as the editor's simulator does), flow analytics and `/v1/flow_templates` come next.
