Tools & Function Calling
Complete architectural guide to function calling, parameter type schemas, sync/async dispatch, and error isolation in chatflow-agent.
Tools empower agents to interact with external environments, execute computation, query databases, and trigger business actions. In chatflow-agent, tools are first-class objects created via the @tool decorator or directly instantiated with the Tool class.
The framework provides an automated introspection engine that translates native Python type annotations and docstrings into compliant OpenAPI and JSON Schema declarations for Google Gemini, OpenAI, and Anthropic models without requiring manual schema definitions.
Defining Tools with @tool
The standard way to author a tool is using the @tool decorator. You can apply it bare or customize its name and description:
1. Bare Decorator
Infers the tool name from the function identifier and description from the function docstring:
# [min-version: 0.3.0]
# [executable]
from chatflow_agent import tool
@tool
def calculate_vat(subtotal: float, rate: float = 0.20) -> float:
"""Calculate Value-Added Tax for an order subtotal."""
return round(subtotal * rate, 2)
assert calculate_vat.name == "calculate_vat"
assert "Calculate Value-Added Tax" in calculate_vat.description2. Parameterized Decorator
Explicitly overrides the name and description exposed to the LLM:
# [min-version: 0.3.0]
# [executable]
from chatflow_agent import tool
@tool(
name="lookup_stock_ticker",
description="Fetch the latest real-time market quote for a stock symbol.",
)
def fetch_quote(symbol: str) -> dict:
"""Internal function to retrieve stock prices."""
# Queries finance service
return {"symbol": symbol.upper(), "price": 182.50, "currency": "USD"}
assert fetch_quote.name == "lookup_stock_ticker"Type Mapping & Parameter Schema Introspection
When a tool is registered, chatflow-agent inspects its signature via Python's standard inspect module and constructs a parameter schema:
| Python Annotation | Canonical Schema Type | Required If |
|---|---|---|
str | "STRING" | No default value assigned |
int | "INTEGER" | No default value assigned |
float | "NUMBER" | No default value assigned |
bool | "BOOLEAN" | No default value assigned |
list | "ARRAY" | No default value assigned |
dict | "OBJECT" | No default value assigned |
Unannotated / Any | "STRING" (Fallback) | No default value assigned |
Parameters with default values (e.g., rate: float = 0.20) are marked as optional in the schema, while parameters without defaults are placed in the required array.
Docstring Best Practice
Language models decide whether to invoke a tool based on its name and description. Always write concise, unambiguous docstrings specifying the exact purpose and expected arguments.
Synchronous vs. Asynchronous Tool Execution
chatflow-agent is designed async-first, but supports both synchronous (def) and asynchronous (async def) tools interchangeably.
import asyncio
from chatflow_agent import tool
# Synchronous tool
@tool
def sync_reverse(text: str) -> str:
"""Reverse a string synchronously."""
return text[::-1]
# Asynchronous tool
@tool
async def async_fetch_status(service_name: str) -> dict:
"""Query service health asynchronously without blocking."""
await asyncio.sleep(0.01) # Simulates async network I/O
return {"service": service_name, "status": "healthy"}Execution Methods
await tool.execute_async(**kwargs)(Recommended):- If the tool is
async def, it is directly awaited on the current event loop. - If the tool is synchronous
def, it is automatically dispatched to an executor worker thread viaasyncio.to_thread. This ensures that heavy calculations, disk I/O, or blocking network libraries never freeze the main async event loop.
- If the tool is
tool.execute(**kwargs)(Synchronous Convenience):- Executes synchronous tools directly.
- If called on an
async deftool outside an event loop, runs it usingasyncio.run().
Event Loop Guard
Calling tool.execute() synchronously on an async def tool from within an already running event loop (e.g., inside FastAPI, Telegram handlers, or Runner turns) is not permitted by Python and will raise a RuntimeError. Always use await tool.execute_async() in asynchronous code.
Version Differences & Prerelease Features
Tool schema generation and execution have evolved significantly between releases:
Stable Release (v0.3.4)
- Stable (0.3.4) Canonical Gemini declaration format (
to_gemini_declaration()). - Single-level type mapping for primitive types and unparameterized
list/dict. - Sync functions offloaded via
asyncio.to_threadduringexecute_async().
Prerelease Enhancements (v0.4.0rc1)
- Prerelease (0.4.0rc1) Deep Collection Typing: Introspects item schemas for
list[T],Sequence[T],set[T], and homogeneous tuplestuple[T, ...]:
from chatflow_agent import tool
@tool
def sum_metrics(values: list[float]) -> float:
"""Sum a list of numeric measurements."""
return sum(values)
# Generates: {"type": "ARRAY", "items": {"type": "NUMBER"}}- Prerelease (0.4.0rc1) Union & Optional Support:
Optional[T]orT | Nonegenerates{ "type": "...", "nullable": True }.int | floatbroadens deterministically to"NUMBER"regardless of declaration order.- Multi-type unions generate an
anyOfschema list.
- Prerelease (0.4.0rc1)
to_openai_parameters(): Generates JSON Schema formatted parameters (lowercased type names,type: "null", and nonullablekeyword) for OpenAI and Anthropic engines. - Prerelease (0.4.0rc1)
ToolSchemaError: Validates signatures at definition time, raising a descriptiveToolSchemaErrorif an unsupported type cannot be mapped into a valid schema. - Prerelease (0.4.0rc1) Varargs Exclusion:
*argsand**kwargsare automatically omitted from parameter schemas.
Error Isolation & Fault Tolerance
In production, tools may encounter network timeouts, invalid database queries, or unexpected exceptions.
chatflow-agent isolates tool failures within the execution turn. If a tool raises an unhandled exception during execution:
- The exception is intercepted and wrapped in a
ToolExecutionError. - The
Runnercatches the failure and injects aToolResultinto conversation history marked withis_error=True:# Injected into message history for the LLM: # ToolResult(tool_call_id="call_123", name="query_db", content="Database connection timeout", is_error=True) - Instead of crashing the entire user conversation, the active agent sees the error result and can explain the issue to the user, retry with alternative parameters, or hand off to a human support queue.
Complete Example: Multi-Tool Agent
from chatflow_agent import Agent, Runner, tool
from chatflow_agent.core.memory import SessionStore
# 1. Define modular domain tools
@tool
def calculate_shipping(weight_kg: float, destination: str) -> dict:
"""Calculate shipping estimate based on weight and country destination."""
rate_per_kg = 8.50 if destination.upper() in ["US", "CA"] else 14.00
total = round(weight_kg * rate_per_kg, 2)
return {"destination": destination.upper(), "cost_usd": total, "delivery_days": 3}
@tool
def check_inventory(sku: str) -> dict:
"""Check stock availability for a product SKU."""
return {"sku": sku, "in_stock": True, "warehouse": "east-region-1"}
# 2. Bind tools to agent
logistics_agent = Agent(
name="LogisticsCoordinator",
instructions="Help customers check inventory availability and calculate shipping rates.",
tools=[calculate_shipping, check_inventory],
model="gemini-2.5-flash",
)
# 3. Create runner with session store
store = SessionStore()
runner = Runner(starting_agent=logistics_agent, session_store=store)
# 4. Execute turn
response = runner.run(
session_id="customer-turn-42",
user_message="Can you check stock for SKU-990 and estimate shipping 2.5 kg to CA?",
)
print("Agent Response:", response.content)
print("Tools Executed:", response.tool_calls_executed)Common Pitfalls & Anti-Patterns
1. Forgetting Type Annotations
Parameters without type annotations fall back to "STRING". If your tool expects numbers or booleans, always include type hints:
# [INCORRECT]: "count" will be exposed as STRING, requiring manual int casting
@tool
def repeat_phrase(phrase, count): ...
# [CORRECT]: Fully typed parameters produce exact OpenAPI validation
@tool
def repeat_phrase(phrase: str, count: int) -> str:
return phrase * count2. Blocking Network Calls in Synchronous Tools
While synchronous tools are dispatched to worker threads via asyncio.to_thread, heavy blocking network operations without timeouts can exhaust Python's thread pool. Whenever possible, author network I/O tools using async def with explicit timeouts.
3. Modifying Global State in Tool Functions
Tools should be pure functions or interact with external storage through thread-safe clients. Avoid mutating global variables, as multiple user sessions can invoke the same tool concurrently.
Agents & Dynamic Instructions
Deep dive into the Agent abstraction, dynamic runtime instruction callables, tool bindings, and configuration parameters in chatflow-agent.
Multi-Agent Handoffs
Orchestrating autonomous swarms, peer-to-peer delegation, synthetic transfer tools, and cycle prevention in chatflow-agent.