Pipeline Integration
Function calling works seamlessly within your existing pipeline structure. The LLM service handles function calls automatically when they’re needed:- User asks a question requiring external data
- LLM recognizes the need and calls appropriate function
- Your function handler executes and returns results
- LLM incorporates results into its response
- Response flows to TTS and user as normal
Understanding Function Calling
Function calling allows your bot to access real-time data and perform actions that aren’t part of its training data. For example, you could give your bot the ability to:- Check current weather conditions
- Look up stock prices
- Query a database
- Control smart home devices
- Schedule appointments
- You define functions the LLM can use and make them available to the LLM service used in your pipeline
- When needed, the LLM requests a function call
- Your application executes any corresponding functions
- The result is sent back to the LLM
- The LLM uses this information in its response
Implementation
1. Define a tool
A tool needs two things: a handler — the code to run when the LLM calls the tool — and a schema that describes the tool to the LLM (its name, what it does, and its parameters) so the model knows it exists and how to call it. The preferred way to define a tool is with a direct function: a single async function that is both the handler and the schema. Pipecat auto-derives the tool’s metadata — name, description, parameter properties (with their descriptions), and which parameters are required — from the function’s signature and docstring. The first parameter is alwaysparams (a FunctionCallParams); the tool’s own arguments follow. Document each argument 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 either “celsius” or “fahrenheit”’), as shown above.
If you need a strict enum in the schema, use the verbose
FunctionSchema pattern.2. Add the tool to the context
List your direct functions inLLMContext(tools=[...]):
system_instruction in the LLM service’s Settings, not as a context message. Tools are automatically converted to the correct format for your LLM provider through adapters.
3. Create the pipeline
Include your LLM service in the pipeline:Per-Tool Options with @tool_options
By default, a direct function is cancelled if the user interrupts, and it uses the LLM service’s global timeout. To override either, decorate the function with @tool_options. The decorator only attaches call options — the schema is still auto-derived — so decorated functions can stay at module level.
cancel_on_interruption(defaultTrue): WhenTrue, the call is cancelled if the user interrupts. WhenFalse, the call is treated as asynchronous — see below.timeout_secs(defaultNone): Per-tool timeout in seconds. Overrides the globalfunction_call_timeout_secsfor this function. Use a longer timeout for slow operations (e.g. database queries) or a shorter one for quick lookups.
@tool_options also sets call options on the handler of a
FunctionSchema tool, not
just a direct function.On an
LLMWorker, mark
tool methods with @tool instead. It applies the same options and marks the
method for automatic collection as one of the worker’s own tools.Synchronous vs. asynchronous calls
Withcancel_on_interruption=True (the default), the call is synchronous: the LLM waits for the result before generating its next response. This ensures the LLM has complete information before responding.
With cancel_on_interruption=False, the call is asynchronous: the LLM continues the conversation immediately without waiting. Once the result returns, it’s injected back into the context as a developer message, triggering a new LLM inference at that point. This enables truly non-blocking calls where the conversation proceeds while the function runs in the background. Async calls can also send intermediate updates before their final result.
Async function call cancellation
For async functions (cancel_on_interruption=False), you can also enable model-directed cancellation:
enable_async_tool_cancellation=True and at least one async function is available, Pipecat automatically adds the built-in cancel_async_tool_call tool and supporting system instructions. The LLM can call that tool to cancel a stale in-progress async function call — for example, when the user changes their request before a long-running lookup completes.
Changing Tools Mid-Conversation
To change the set of tools the LLM can use during a session, push anLLMSetToolsFrame. Its tools field takes the same things as LLMContext(tools=[...]) — a list of direct functions and/or FunctionSchema objects. Whatever you pass becomes the LLM’s new tool set.
Tools Across Service Switches
When you use anLLMSwitcher to swap LLM providers mid-session, the tools you list in LLMContext(tools=[...]) are available on whichever provider is active. You define them once for the whole switcher.
Advanced: Defining Tools with FunctionSchema
Direct functions cover most cases. Reach for the verbose FunctionSchema pattern when you need explicit control over the schema — for example, a strict enum constraint (which the direct-function generator doesn’t yet emit) — or when the tool’s handler isn’t shaped like a direct function.
A FunctionSchema spells out the tool’s name, description, and parameters by hand. Pass the handler that runs when the LLM calls the tool as the schema’s handler, then list the schema in LLMContext(tools=[...]) — exactly as you would a direct function.
@tool_options — the same decorator direct functions use, with the same synchronous vs. asynchronous semantics:
LLMSetToolsFrame, and they keep working across an LLMSwitcher’s providers.
Registering a handler manually
Bundling the handler on the schema (above) is the recommended approach. If you’d rather keep the handler separate, list a handler-freeFunctionSchema in the context as usual and register its handler by name:
LLMSetToolsFrame; call llm.unregister_function(...) only afterward, since unregistering a still-advertised tool leaves the LLM able to call a handler that’s no longer there.
This is uncommon — bundling keeps a tool and its handler together — but the option is there when you need to manage registration directly.
Provider-Specific Custom Tools
For normal function calling, preferstandard_tools with FunctionSchema or direct functions so Pipecat can convert them to each provider’s native format. When a provider has tools that don’t fit Pipecat’s standard function schema, add those provider-native definitions through ToolsSchema.custom_tools. These custom tools are passed only to the matching adapter and are appended to the converted standard tools.
Raw provider-native tool lists are not the normal
LLMContext path. Some
lower-level adapter code still preserves non-ToolsSchema tools for legacy or
direct provider-specific paths, but LLMContext(tools=...) validates tools as
a ToolsSchema. Use custom_tools as the provider-specific escape hatch
while staying in the universal context flow.Function Handler Details
FunctionCallParams
Every function handler receives aFunctionCallParams object containing all the information needed for execution:
params.tool_resources is a deprecated alias for params.app_resources. Use
app_resources in new code.Handler Structure
Your function handler should:- Receive necessary arguments, either:
- From
params.arguments - Directly from function arguments, if using direct functions
- From
- Process data or call external services
- Return results via
params.result_callback(result)
Sharing Resources with app_resources
When function handlers need access to shared resources like database connections, API clients, or application state, you can pass them viaapp_resources when creating the PipelineWorker. These resources are then accessible in every function handler via params.app_resources.
- Resources are passed by reference — the caller retains their handle and can read mutations after the task finishes
- The framework never copies or clears the
app_resourcesobject - All function handlers in the pipeline share the same
app_resourcesinstance - Useful for database connections, API clients, caches, or any shared state
PipelineWorker(tool_resources=...) and FunctionCallParams.tool_resources
are deprecated aliases retained for compatibility. Prefer
PipelineWorker(app_resources=...) and params.app_resources.Advanced: Controlling Function Call Behavior
When returning results from a function handler, you can control how the LLM processes those results using aFunctionCallResultProperties object passed to the result callback.
Properties
FunctionCallResultProperties provides fine-grained control over LLM execution:
run_llm=True: Run LLM after function call (default behavior)run_llm=False: Don’t run LLM after function call (useful for chained calls)on_context_updated: Async callback executed after the function result is added to contextis_final=False: Treat this as an intermediate result for an async function call. Only use this for async functions (cancel_on_interruption=False)
Example Usage
Intermediate Results for Async Functions
Async function calls can send progress updates before their final result. Make the function async with@tool_options(cancel_on_interruption=False), then call params.result_callback(..., properties=FunctionCallResultProperties(is_final=False)) for each intermediate update. Finish with a normal params.result_callback(...).
Key Takeaways
- Function calling extends LLM capabilities beyond training data to real-time information
- Context integration is automatic - function calls and results are stored in conversation history
- Direct functions are the preferred approach - one async function is both schema and handler; list it in
LLMContext(tools=[...])or add it viaLLMSetToolsFrameto make it available. When you need explicit schema control, use aFunctionSchemawith itshandlerbundled in - Async function calls are opt-in - set
cancel_on_interruption=Falsefor deferred results, intermediate updates, and optional async-tool cancellation - Pipeline integration is seamless - functions work within your existing voice AI architecture
- Advanced control available - fine-tune LLM execution and monitor function call lifecycle
What’s Next
Now that you understand function calling, let’s explore how to configure text-to-speech services to convert your LLM’s responses (including function call results) into natural-sounding speech.Text to Speech
Learn how to configure speech synthesis in your voice AI pipeline