Skip to main content
Functions in Pipecat Flows serve two key purposes:
  1. Process data by interfacing with external systems and APIs to read or write information
  2. Progress the conversation by transitioning between nodes in your flow

How Functions Work

When designing your nodes, clearly define the task in the task_messages and reference the available functions. The LLM will use these functions to complete the task and signal when it’s ready to move forward. For example, if your node’s job is to collect a user’s favorite color:
  1. The LLM asks the question
  2. The user provides their answer
  3. The LLM calls the function with the answer
  4. The function processes the data and the conversation moves on
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 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:
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.
str | Branch
The node to transition to after the tool completes, or a branch table. Omit it for a node function, which stays put.
bool
default:"false"
Whether the tool is defined here rather than in code. See below.
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.

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.
handlers.py
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.
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.
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:
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:
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.
dict[str, str]
required
Result value to node name. At least one entry.
str
Node to transition to when the value matches no case. When omitted, an unmatched value stays on the current node.
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:

The Shim Pattern

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 that reports the outcome as a named field, and let the config route on that field. check_availability in the restaurant reservation example is the shape:
handlers.py
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:
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 pattern.

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:
The handler is required. It receives the LLM-supplied arguments and returns the same result and next-node 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: