> ## Documentation Index
> Fetch the complete documentation index at: https://docs.goyappr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Flow Config Schema

> JSON shape for `agents.flow_config` — node types, transitions, validation rules.

Flow agents (`type: "flow"`) carry a `flow_config` JSONB on the agent row. This page is the canonical reference for that shape. The same schema is validated server-side by the public API, by the dashboard's React Flow builder, and by the bot pipeline at call-start time.

## Top-level shape

```json theme={null}
{
  "flow_config_version": "1",
  "nodes": [ /* see node types below */ ],
  "metadata": { /* optional, surfaced to the eval LLM */ }
}
```

### Validation gates (enforced before save)

* Exactly **one** node with `type: "start"`.
* Every `next_step_id` (transitions, custom branches, start) resolves to an existing node id.
* Node ids are unique within the graph.
* Every node passes its own per-type schema (see below).
* Every conversation transition has a non-empty `description` (`transition_description_missing`) — the natural-language trigger the voice agent uses to pick this path.

A failed gate returns `400` with a human-readable `error` message pointing at the offending field.

## Node types

Every node has:

```json theme={null}
{
  "id": "stable-id",
  "type": "<one of: start | conversation | tool_call | integration_call | transfer | end>",
  "name": "Display name (canvas label)",
  "position": { "x": 0, "y": 0 }   // optional, UI-only
}
```

### `start`

Entry point. Owns the "who speaks first" decision for flow agents — these settings **override** the agent-level `agent.agent_speaks_first` and `agent.greeting_message` fields when the agent is a flow agent.

```json theme={null}
{
  "id": "start-1",
  "type": "start",
  "agent_speaks_first": true,
  "greeting": "שלום, חברת ירוק מדבר",
  "is_literal": false,
  "next_step_id": "ask-name-1",
  "auto_advance": true
}
```

| Field                | Description                                                                                                                                                                                                                                                               |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_speaks_first` | Default `true`. When `false`, the bot waits silently for the caller to speak first (`silence_timeout_secs` on the agent still applies).                                                                                                                                   |
| `greeting`           | Spoken greeting — only used when `agent_speaks_first: true`.                                                                                                                                                                                                              |
| `is_literal`         | When `true`, `greeting` is spoken verbatim. When `false`, it's treated as an LLM instruction.                                                                                                                                                                             |
| `next_step_id`       | The conversation node the bot enters after greeting (or after the user's first turn, if `agent_speaks_first: false`).                                                                                                                                                     |
| `auto_advance`       | Default `true`. Whether to enter the first conversation node immediately on session start. When `false`, the bot's greeting is delivered in start-node context only; the first conversation node is entered only after the user's first reply (via internal advancement). |

<Note>
  Use `auto_advance: false` when the greeting should feel neutral and the bot
  shouldn't enter its first scripted step until the user has spoken. Useful for
  agents that listen for an open-ended intent (caller's reason for calling)
  before routing — the greeting stays generic, then the first conversation
  node's instructions take effect once the caller actually replies.
</Note>

### `conversation`

Talks to the user, then picks a transition.

```json theme={null}
{
  "id": "ask-name-1",
  "type": "conversation",
  "name": "Ask name",
  "instructions": "Ask the caller for their full name. Confirm the spelling if Hebrew sounds ambiguous.",
  "transitions": [
    {
      "id": "tx-got-name",
      "label": "Caller gave their name",
      "description": "Caller stated their full name (first and last) clearly enough to record.",
      "next_step_id": "ask-date-1"
    },
    {
      "id": "tx-refused-name",
      "label": "Caller refused",
      "description": "Caller declined to give their name, said they don't want to share it, or asked why you're asking.",
      "next_step_id": "polite-end-1"
    }
  ]
}
```

The `instructions` text is layered on top of the agent's global `system_prompt` as a system message at step entry. **Every transition must have a non-empty `description`** — at runtime the voice agent reads the description as the trigger for taking that path. Treat the description as a prompt addressed to the model: describe the **user-side signal** (what the caller said or implied), not an agent intent. A bare `label` is not sufficient; the API rejects empty descriptions with `transition_description_missing`.

### `tool_call`

Deterministic tool execution. References a tool by `tool_id` (must already exist in the company's `tools` table). Tool args are owned by the **tool itself** via `payload_config.static_parameters` (literals) and `payload_config.extraction_parameters` (filled by the runtime from the conversation). A `tool_call` node carries **no** `args_template` field — the same tool used by N flow nodes sends the same shape unless that node supplies a `config_override`. Stale `args_template` payloads on a `tool_call` node are silently dropped at parse time.

```json theme={null}
{
  "id": "check-avail-1",
  "type": "tool_call",
  "name": "Check Availability",
  "tool_id": "11111111-1111-1111-1111-111111111111",
  "config_override": null,
  "transitions": {
    "success_next_step_id": "confirm-1",
    "error_next_step_id": "apologize-1",
    "custom": [
      {
        "id": "tx-no-slots",
        "label": "No availability",
        "jsonpath": "$.status",
        "equals": "no_availability",
        "next_step_id": "offer-alt-1"
      }
    ]
  },
  "pre_fire_announcement": true,
  "timeout_secs": 30
}
```

`config_override` shallow-merges over the referenced tool's `config` (array replacement, not deep merge). Open-shape: each tool type defines its own valid keys.

The dashboard flow editor shows the shared base configuration, the fields overridden by this node, and the final effective configuration together in the node editor. Because the merge is shallow, overriding `payload_config` replaces that complete object; include every standard-metadata, static-parameter, and extraction-parameter setting the node needs. Switching a node to another tool clears the previous tool's override after confirmation.

`pre_fire_announcement` plays a short platform-controlled hold tone while the webhook is in flight. Recommended for tools that may take more than \~500 ms.

`timeout_secs` (1–600) is an optional per-node hard cap. When set, the dispatcher cancels the action at that limit and routes to `error_next_step_id` with `tool_timeout_after_<N>s`. When omitted on a webhook tool, the controller allows the tool's effective `config.timeout_seconds` plus one second of dispatch overhead; other tool and integration nodes default to 30 seconds. Set it only when the flow needs a stricter or longer node-level limit.

**Args come from the linked tool, not the node.** At call start, Yappr resolves the referenced tool plus that node's `config_override` and gives the voice model a flat argument-submission schema: one named string field for each effective `extraction_parameters` entry. There is no model-facing `node_id` field and no nested `{ "args": { ... } }` wrapper. The runtime assembles standard call metadata and static parameters; the model-facing submitter exposes only extraction fields.

Payload merge order is standard metadata, then static parameters, then extracted values, so an extracted value wins a deliberate name collision. Keep names unique across all three groups unless that override is intentional.

`extraction_parameters[].required` controls which fields the model must collect. It defaults to `true` when omitted. Missing optional fields do not block dispatch; if a required field still can't be surfaced after 3 attempts, the node routes to `error_next_step_id`.

<Note>
  Tool schemas are fixed for the duration of a live call. Changes to the referenced
  tool or a node's `config_override` are picked up when the next call starts.
</Note>

A flow may define at most **127 unique typed extraction contracts**. Nodes with
the same effective tool, `config_override`, and extraction schema share one
contract; integration nodes participate only for arguments in `ai_extract`
mode. Saves over the limit fail with `too_many_extraction_contracts` instead of
letting the next call fail during model-session startup.

Tool-call nodes can be **token sources** for downstream nodes: any `extraction_parameters[].name` becomes addressable as `{{<node_id>.<name>}}` in later `integration_call.args_template` literals or `ai_extract.description` strings.

**How tool-call routing works at runtime** — entirely deterministic, no LLM involved:

1. **`error_next_step_id` fires only on hard failures**: network timeout, exception, HTTP redirect or other non-2xx status, integration disconnected, tool deleted/inactive, missing required config. The dispatcher's `error` field is set; nothing else is checked.
2. **Otherwise the dispatcher walks `custom[]` top-to-bottom.** First branch whose path extracts a value `==` its `equals` wins. Evaluation **stops** there — `success` is **not** also taken.
3. **If no custom matched, `success_next_step_id` fires.**

Exactly **one** out-edge is traversed per tool fire. The canvas drawing all three lines is just the diagram of *possible* routes — at runtime only one is taken.

The full result dict is injected into the next node's LLM context as a `<tool_result>` block, so a single `success` → conversation node usually handles soft-fail bodies (`{"available": false}`) gracefully via prompt instructions. **Use `custom[]` only when the next node should be *structurally* different** for that shape (different instructions, different downstream tools, different transitions).

#### JSONPath subset for `custom[].jsonpath`

The runtime supports a **deliberately tiny** subset of JSONPath. Root (`$`) is the **tool's parsed response body**:

* Webhook tools — `JSON.parse(body)` of the HTTP response.
* Integration tools — the typed dict returned by the provider client.

| Path                    | Resolves to           |
| ----------------------- | --------------------- |
| `$.status`              | `result["status"]`    |
| `$.data.appointment.id` | nested key drill-down |
| `$.slots[0].time`       | array index           |
| `$.appointments[2]`     | bare array index      |

**Not supported**: recursive descent (`$..foo`), wildcards (`$.*`), filter expressions (`$[?(@.x>1)]`). If your webhook nests the field, point at the exact path. A missing key, wrong type at any step, or out-of-bounds index → the branch silently doesn't match (falls through to the next custom, then to `success`).

#### Stringification rules for `equals`

The extracted value is stringified JSON-style before string-comparison to `equals`. Match the table:

| Result value        | Write `equals:`      |
| ------------------- | -------------------- |
| `true` (boolean)    | `"true"` (lowercase) |
| `false` (boolean)   | `"false"`            |
| `null`              | `"null"`             |
| `42` (number)       | `"42"`               |
| `"booked"` (string) | `"booked"`           |

Branches with the wrong stringification (e.g. `equals: "True"` against a boolean `true`) silently never match.

### `integration_call`

Calls an OAuth-backed third-party integration (Google Calendar, Gmail) directly from the flow — no `tools` table row required. The integration config (provider, credential, action) lives on the node itself and the args go in `args_template`. Routing is identical to `tool_call` (success / error / custom JSONPath, mutually exclusive, exactly one out-edge per fire).

For the full per-node schema, action catalog, arg modes (literal / ai\_extract with `{{node.arg}}` and `{{metadata.key}}` token interpolation), and the Calendar response post-processing rules, see the dedicated reference: [Integration call node](/api-reference/flows/integration-call-node).

### `transfer`

Hands the call off via SIP transfer. Terminal — no transitions out.

```json theme={null}
{
  "id": "transfer-1",
  "type": "transfer",
  "transfer_to": "+972501234567",
  "transfer_message": "Connecting you now"
}
```

### `end`

Speaks the farewell and hangs up. Terminal.

```json theme={null}
{
  "id": "end-1",
  "type": "end",
  "farewell": "Thanks for calling, goodbye",
  "is_literal": false
}
```

For per-call extraction or webhook delivery, configure `extraction_parameters` and `webhook_url` / `webhook_events` at the **agent level**. Those mechanisms apply uniformly to both prompt and flow agents — the flow does not have a separate post-end pipeline.

## Global nodes

Conversation, transfer, and end nodes can be marked **`is_global: true`** to make them reachable from any conversation node without an explicit edge. The eval LLM gets every global node as an extra candidate transition on every turn, with a strong "prefer labeled transitions over global jumps when both could plausibly apply" bias.

Use cases:

* **Misclassification recovery** — "wait, you're an owner, not a tenant" routing to the owner-branch entry, declared once instead of wired into every node.
* **Universal escape hatches** — `transfer` to human, or `end` on do-not-call.
* **Session-level concerns** — "callback request from anywhere".

```json theme={null}
{
  "id": "owner_intake",
  "type": "conversation",
  "name": "Owner intake",
  "instructions": "...",
  "transitions": [...],
  "is_global": true,
  "global_jump_description": "User reveals they're actually an owner/landlord, not a tenant"
}
```

**Validation rules** (enforced by the API on POST/PATCH):

* `is_global: true` is **only** valid on `conversation`, `transfer`, and `end` nodes. Setting it on a `start` or `tool_call` node returns 400.
* When `is_global: true`, `global_jump_description` must be a non-empty string.
* The current node never jumps to itself even if it's global.

**Practical guidance**:

* Recommended max ≤3 globals per flow. Each additional global widens the eval LLM's candidate space and increases false-positive jump risk.
* Write `global_jump_description` as a **user-side signal**, not an agent intent. Good: "User says they want to speak to a human"; bad: "Transfer the user".
* Globals don't replace explicit transitions — they're a fallback. The eval LLM is instructed to prefer labeled transitions when ambiguous.

When a global jump fires, the runtime publishes `flow_node_entered` with `reason: "global jump: <node name>"` (visible in `flow_trace.steps[].reason` on `GET /calls/:id`).

## What's NOT a node type

By design, the flow data model omits a few patterns you might expect:

* **No "webhook" node** — for outbound webhooks, either (a) configure `agent.webhook_url` + `agent.webhook_events` for unconditional per-call delivery (recommended), or (b) call a webhook-type tool from a `tool_call` node mid-flow if you need per-path delivery.
* **No "structured\_output" node** — for transcript extraction, configure `agent.extraction_parameters`. (Per-end-path schemas were considered for v1 but cut to avoid duplicating an agent-level mechanism.)
* **No separate "branch" / "ifelse" node** — branching is built into every conversation node via `transitions[]`.

## Versioning

Every successful save (POST or PATCH that includes `flow_config`) writes a `flow_versions` row. Identical re-saves are deduplicated by SHA-256 hash of the canonical JSON. Use `GET /agents/:id/flow/versions` to list history.

## Immutability

`agents.type` is **immutable** post-create. To convert a prompt agent into a flow agent (or vice-versa), create a new agent of the desired type. PATCHing `type` returns `400`.

## Type discriminator and `flow_config` requirements

| `type`   | `flow_config` required? | `flow_config` allowed?    |
| -------- | ----------------------- | ------------------------- |
| `prompt` | no                      | no (must be omitted/null) |
| `flow`   | yes                     | yes                       |

Mismatches return `400` with a clear correction message.
