Skip to main content
A flow config describes a conversation as data: the nodes, what each one says, which tools each node offers, and where each tool leads. It contains no Python. The tools it names live in your code and are resolved when the config is joined to your handlers. Writing the flow this way puts a seam between the graph and the bot: the handlers ship with the code, and the flow can be loaded per session and changed without a deploy. The overview explains when to choose it.

The Document

A config has an initial_node, a map of nodes keyed by name, and an optional list of global_functions. Here is the food-ordering example in full:
flow.yaml
The full example, with its complete prompts and its handlers.py, is in examples/flows/yaml/food_ordering/.

Keys

At the top level, initial_node names the node the flow starts in, nodes holds the nodes keyed by name, and global_functions lists tools offered at every node. The key under nodes is the node’s name: it is what initial_node and every transition_to refer to. A node has:
  • task_messages (required): what the LLM should do at this node, each entry with a role and content.
  • role_message: the bot’s role, sent as the LLM’s system instruction and kept until another node sets its own.
  • functions: the tools the node offers. See Functions.
  • pre_actions and post_actions: see Actions.
  • context_strategy: append or reset. See Context Strategies.
  • respond_immediately: whether the LLM responds as soon as the node is entered. Defaults to true.
role_message and each task message’s content may contain {{ key }} placeholders, filled from the manager’s state each time the node is entered. See Placeholders. Every field, with its validation rules, is in the FlowConfig reference.

Loading a Config

FlowConfig is a Pydantic model, so model_validate is the loader for a dict you built or fetched yourself.

Splitting Out Long Prompts

A YAML config loaded with from_file can pull text in from another file with !include, resolved relative to the config’s own directory. This keeps a long prompt out of the graph:
!include is available with from_file, and with from_yaml when you pass base_dir. It is not available in JSON.

Validation

A config is checked in two stages. On load, its structure: the top level is a mapping, initial_node names a defined node, every transition_to names a defined node, tool names are unique within a node and across global_functions, a transition_only entry has both a description and a plain node name to transition to, an ordinary entry has no description, and every action is well-formed. A failure raises a Pydantic ValidationError. When the Flow is constructed, its references into your code: every tool it names exists in the handlers, is callable, and is a valid direct function; every action handler it names exists. These are collected and reported together as a single FlowReferenceError, rather than stopping at the first, so one run surfaces every miss.
Each problem is also available on the exception as a FlowProblem with a stable code, so you can handle them programmatically.
Because both stages run before the first call comes in, starting the bot once is a complete check of the flow.

Joining a Config to Code

A Flow is a config joined to the Python it names:
handlers can be:
  • A module, as above — the usual case. Its top-level functions are looked up by name.
  • A mapping of names to callables, when you want to build the namespace yourself or expose a tool under a different name than the function has.
  • A list or tuple of either, when tools and action handlers live in separate modules.
Only the names the config actually references are looked up, so an unrelated function in the module is ignored. With a list, a name that resolves to different callables in more than one entry is an error rather than a silent choice; the same callable reachable through two of them is fine. The flow then hands three things to the FlowManager:
NodeConfig
The node the flow starts in, ready to pass to FlowManager.initialize().
list
The config’s global functions, ready to pass to FlowManager(global_functions=...). A fresh list each time, so you can extend it with tools defined in code.
NodeConfig
Any node by name, for the rare case where code needs to jump into the graph directly. Raises FlowError if the config has no such node.

Loading a Flow Per Session

Because the config is just a document, the bot doesn’t have to ship with it. Fetch the flow a session calls for, seed the facts its prompts refer to, and initialize:
The handlers stay the same across every flow the bot can run. What changes per session is the document: which nodes exist, what they say, and where each tool leads.
A config fetched from outside your codebase can still name a tool your handlers don’t have. Construct the Flow where you can catch FlowReferenceError and fail the session cleanly, rather than mid-call.

Tooling

The config format’s JSON Schema is published in the Pipecat repository at src/pipecat/flows/flow_config.schema.json. Point your editor at it for completion and inline validation while writing a config, or vendor it into a tool that generates one. The Pipecat Flows Visual Editor lets you design a flow visually rather than by hand.

Next Steps

Functions

Tool entries, transition-only functions, and branch tables

State Management

Placeholders, cross-node state, and global functions

Actions

Built-in and custom actions in a config

FlowConfig Reference

Every field, loader, and validation rule