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

# Functions

> Define Pipecat Flows tools: node and edge functions, transition_only entries, branch tables, and the direct-function handler contract.

A function is a tool the LLM can call. It does work, such as reading or writing an external system, moves the conversation to another node, or both. A node's `task_messages` describe the task and name the functions it may use; when the LLM has what it needs, it calls one, and the conversation moves on.

## Node and Edge Functions

A function is one of two kinds, in either form of flow:

* A **node function** does work within the current conversation state without switching nodes.
* An **edge function** transitions to another conversation state, optionally doing work first.

What differs between the two forms is *who decides* which kind a function is. In a [flow config](/pipecat/flows/flow-configs) the entry decides: an entry with `transition_to` is an edge function. In code the handler decides, by returning the next node or not.

## Declarative

A `functions` entry names a tool and, optionally, where it leads:

```yaml theme={null}
functions:
  - name: select_pizza_order
    transition_to: confirm
```

<ParamField path="name" type="str" required>
  The tool's name, as the LLM sees it, and the name of the direct function in
  the handlers. The config carries no description or parameters for an ordinary
  entry — those come from the function's signature and docstring.
</ParamField>

<ParamField path="transition_to" type="str | Branch">
  The node to transition to after the tool completes, or a [branch
  table](#branch-tables). Omit it for a node function, which stays put.
</ParamField>

<ParamField path="transition_only" type="bool" default="false">
  Whether the tool is defined here rather than in code. See
  [below](#transition-only-functions).
</ParamField>

<ParamField path="description" type="str">
  What the tool is for, for the LLM. Only a `transition_only` entry takes one;
  an ordinary entry describes itself in its docstring, and giving it a
  description here is an error.
</ParamField>

### The Handler Contract

The Python behind an ordinary entry is a **direct function**: a single async function that is *both* the handler and the schema. Flows derives the tool's description, parameter properties, and which parameters are required from the signature and a Google-style docstring. The first parameter is always `flow_manager`; the function's own parameters follow.

```python handlers.py theme={null}
from pipecat.flows import TRANSITION_IN_YAML, FlowManager


async def select_pizza_order(flow_manager: FlowManager, size: str, pizza_type: str):
    """Record the pizza order details.

    Args:
        size (str): Size of the pizza. Must be one of "small", "medium", or "large".
        pizza_type (str): Type of pizza. Must be one of "pepperoni", "cheese", "supreme", or "vegetarian".
    """
    price = {"small": 10.00, "medium": 15.00, "large": 20.00}[size]
    flow_manager.state["order"] = {"size": size, "type": pizza_type, "price": price}
    return {"size": size, "type": pizza_type, "price": price}, TRANSITION_IN_YAML
```

A handler in a declarative flow returns `(result, TRANSITION_IN_YAML)`. The result is any JSON-serializable value, or `None`, and is given to the LLM as context. `TRANSITION_IN_YAML` is a sentinel meaning "the config decides" — it is what keeps transitions out of your Python.

<Warning>
  Returning anything else in the next-node slot raises a `FlowError` at call
  time. A node name string, a `NodeConfig`, or `None` are all rejected: in a
  declarative flow the config owns transitions, so there is nothing for the
  handler to decide. Return `TRANSITION_IN_YAML` even from a node function that
  stays put — the config's missing `transition_to` is what makes it stay.
</Warning>

To leave the bot silent after the tool finishes, rather than responding immediately, return `(result, NO_RESPONSE)`. That passes through to the manager unchanged.

### Transition-Only Functions

A function that only moves the conversation needs no Python. Mark the entry `transition_only: true` and give it a `description` for the LLM and a node name to transition to:

```yaml theme={null}
functions:
  - name: choose_pizza
    transition_only: true
    description: The caller wants to order pizza.
    transition_to: pizza
```

It takes no parameters and runs no code. A `transition_only` entry must have both a `description` and a `transition_to` that names a node — a branch table needs a result to branch on, and there is none.

### Branch Tables

When the destination depends on the tool's outcome, `transition_to` can be a **branch table** that routes on a field of the result:

```yaml theme={null}
functions:
  - name: check_availability
    transition_to:
      field: status
      cases:
        available: confirm
        unavailable: no_availability
```

<ParamField path="field" type="str" required>
  Key of the tool's result whose value selects the case. The result must be a
  mapping with that key, or the call raises a `FlowError`.
</ParamField>

<ParamField path="cases" type="dict[str, str]" required>
  Result value to node name. At least one entry.
</ParamField>

<ParamField path="default" type="str">
  Node to transition to when the value matches no case. When omitted, an
  unmatched value stays on the current node.
</ParamField>

Case keys may be written as strings, booleans, or numbers. They are matched against the result value by a canonical string, so `true:` in YAML matches a Python `True`, and `"True":` matches it too:

```yaml theme={null}
functions:
  - name: verify_birthday
    transition_to:
      field: verified
      cases:
        true: get_prescriptions
```

### Routing on Business Logic

A branch table routes on a field, not on arbitrary logic, and that is the point. Keep the business logic flow-agnostic, wrap it in a thin tool, a shim, that reports the outcome as a named field, and let the config route on that field.

`check_availability` in the [restaurant reservation example](https://github.com/pipecat-ai/pipecat/tree/main/examples/flows/yaml/restaurant_reservation) is the shape:

```python handlers.py theme={null}
class MockReservationSystem:
    """Business logic. Knows nothing about the flow."""

    async def check_availability(
        self, party_size: int, requested_time: str
    ) -> tuple[bool, list[str]]:
        ...


reservation_system = MockReservationSystem()


async def check_availability(flow_manager: FlowManager, time: str, party_size: int):
    """Check availability for requested time.

    Args:
        time (str): Requested reservation time in "HH:MM AM/PM" format. Must be between 5 PM and 10 PM.
        party_size (int): Number of people in the party.
    """
    is_available, alternative_times = await reservation_system.check_availability(
        party_size, time
    )

    # The business logic answered with a bool. Report it as a named status so
    # the config can branch on it; the config, not this tool, picks the node.
    return {
        "status": "available" if is_available else "unavailable",
        "time": time,
        "alternative_times": alternative_times,
    }, TRANSITION_IN_YAML
```

The reservation system stays reusable and testable on its own, the tool stays a few lines, and where "unavailable" leads is a config change.

## Programmatic

In a flow built in code, the handler returns the next node itself. A direct function is written the same way — `flow_manager` first, then its own parameters, documented in a Google-style docstring:

```python theme={null}
async def record_favorite_color(
    flow_manager: FlowManager,
    color: str,
) -> tuple[str, NodeConfig]:
    """Record the color the user said is their favorite.

    Args:
        color: The user's favorite color.
    """
    print(f"Your favorite color is: {color}")
    return color, create_end_node()

# List the function in a node
node_config = NodeConfig(
    # ...
    functions=[record_favorite_color],
)
```

<Note>
  The direct-function schema generator doesn't yet map `Literal` types to a
  JSON-schema `enum`. Express enum-like constraints in the docstring prose
  instead (e.g. *'Must be one of "red", "green", or "blue"'*). If you need a
  strict `enum` in the schema, use the
  [`FlowsFunctionSchema`](#advanced-defining-a-function-with-flowsfunctionschema)
  pattern.
</Note>

### Return Values

A function returns a tuple:

* **Result**: Data provided to the LLM for context in subsequent completions, or `None`. Any JSON-serializable value is accepted.
* **Next Node**: The `NodeConfig` for Flows to transition to next, or `None`.

A node function returns `None` for the next node. One that *only* changes conversational state, without doing other work, can return `None` for the result. Returning `NO_RESPONSE` in the next-node slot keeps the bot silent after the call.

### Advanced: Defining a Function with `FlowsFunctionSchema`

Direct functions cover most cases. Reach for `FlowsFunctionSchema` when you need explicit control over the schema — for example a strict `enum` constraint or a numeric `minimum`/`maximum` — that a direct function can't yet express. This is one of the reasons to write a flow in code rather than a config.

A `FlowsFunctionSchema` spells out the function's name, description, and parameters by hand, and takes the `handler` that runs when the LLM calls the function:

```python theme={null}
from pipecat.flows import FlowsFunctionSchema

async def record_favorite_color(
    args: FlowArgs, flow_manager: FlowManager
) -> tuple[str, NodeConfig]:
    """Record the color, then set the next node."""
    print(f"Your favorite color is: {args['color']}")
    return args["color"], create_end_node()

record_favorite_color_func = FlowsFunctionSchema(
    name="record_favorite_color",
    description="Record the color the user said is their favorite.",
    properties={
        # A strict enum — the kind of explicit control a direct function can't yet express.
        "color": {"type": "string", "enum": ["red", "green", "blue"]},
    },
    required=["color"],
    handler=record_favorite_color,
)

# List the schema in a node
node_config = NodeConfig(
    # ...
    functions=[record_favorite_color_func],
)
```

The `handler` is required. It receives the LLM-supplied arguments and returns the same [result and next-node values](#return-values) as a direct function.

## Per-Function Call Options

By default, a function is not cancelled when the user interrupts, and it uses the LLM service's global timeout. To override either, decorate the handler with `@flows_tool_options`. This works the same in both forms — it is a property of the Python, so a declarative flow's handlers take it too:

```python theme={null}
from pipecat.flows import TRANSITION_IN_YAML, flows_tool_options

# This lookup is only useful for the current turn, so cancel it if the user
# interrupts and the conversation moves on.
@flows_tool_options(cancel_on_interruption=True)
async def check_weather(
    flow_manager: FlowManager,
    city: str,
):
    """Look up the current weather for a city.

    Args:
        city: The city to look up.
    """
    return {"weather": await get_weather(city)}, TRANSITION_IN_YAML
```
