Functions and hooks
LLM-callable functions, side-effect hooks, and transition logic for agent behavior.
Overview
Functions are the tools the LLM can invoke during a conversation. Each node defines its own set of functions, and the LLM decides which to call based on the function’s description. When called, hooks fire as side effects (fire-and-forget), and the conversation optionally transitions to a new node.
FlowFunction fields
| Field | Type | Description |
|---|---|---|
name | string | Unique function name within the node |
description | string | Tells the LLM when to call this function |
properties | JSON Schema | Parameter definitions the LLM should extract |
required | string[] | Required parameter names |
transition_to | Optional[string] | Node to transition to. null = stay on current node |
hooks | List[HookConfig] | Side-effect hooks fired asynchronously |
Description Matters
The description field is critical — it’s what the LLM uses to decide when to invoke the function. Write clear, specific descriptions like “Customer has confirmed the appointment date and time”.
How the LLM uses functions
- The LLM receives node task messages plus available functions (node + global)
- Based on conversation, the LLM decides to call a function and extracts arguments
- Arguments are validated against
propertiesandrequired - Hooks execute asynchronously (non-blocking)
- If
transition_tois set, conversation moves to that node
Function examples
Navigation Function
{
"name": "wants_to_reschedule",
"description": "Customer indicates they cannot make the current appointment and wants to find a new time",
"properties": {},
"required": [],
"transition_to": "reschedule_node",
"hooks": [
{
"name": "update_outcome_in_database",
"expected_fields": {
"outcome": { "source": "static", "value": "reschedule_requested" }
}
}
]
}Data Collection Function
{
"name": "collect_email",
"description": "Customer has provided their email address for confirmation",
"properties": {
"email": { "type": "string", "description": "The customer's email address" },
"consent": { "type": "boolean", "description": "Whether customer consents to emails" }
},
"required": ["email"],
"transition_to": "confirm_details",
"hooks": [
{
"name": "update_outcome_in_database",
"expected_fields": {
"outcome": { "source": "static", "value": "email_collected" },
"email": { "source": "llm" }
}
},
{
"name": "send_http_request",
"expected_fields": {
"email": { "source": "llm" },
"customer_id": { "source": "static", "value": "{customer_id}" }
},
"http_request": {
"url": "https://api.example.com/contacts/update",
"method": "POST",
"headers": { "Authorization": "Bearer {api_token}" },
"body": { "id": "{customer_id}", "email": "<<email>>" }
}
}
]
}Hooks: side effects on functions
Hooks are fire-and-forget side effects that execute via asyncio.create_task when a function is called. They do not block the conversation.
Fire-and-forget — failures are silent to the conversation
Hooks do not block the conversation. If a hook fails, the conversation continues unaffected and the lead’s outcome field keeps its prior value. Find failed hooks in the structured logs: filter by hook_name and ERROR level, or search the log stream for [hook] entries. The lead_call_tracker_id is attached to every log line via contextvars, so filtering by lead_id surfaces every hook that ran for a given call.
HookConfig Fields
| Field | Type | Description |
|---|---|---|
name | string | Hook identifier — determines which handler runs |
expected_fields | Dict[string, FieldConfig] | Map of field names to source config |
http_request | Optional[HttpRequestConfig] | HTTP config (required for send_http_request) |
Available hook types
| Hook Name | Description | Requires http_request |
|---|---|---|
update_outcome_in_database | Updates the lead’s outcome and metadata | No |
send_http_request | Fires an HTTP request to an external service | Yes |
set_transfer_flag | Sets a Redis context flag for warm transfer | No |
Field source types
| Source | Value Field | Description |
|---|---|---|
static | Required | Hardcoded value. Supports {'{variable}'} substitution |
llm | Not used | Value from the LLM function call arguments |
computed | Required | Dynamic runtime value (e.g. utc_now_minus_hours:1) |
{
"expected_fields": {
"outcome": { "source": "static", "value": "confirmed" },
"campaign_id": { "source": "static", "value": "{campaign_id}" },
"preferred_date": { "source": "llm" },
"called_at": { "source": "computed", "value": "utc_now_minus_hours:0" }
}
}HttpRequestConfig
| Field | Type | Default | Description |
|---|---|---|---|
url | string | — | Target URL (supports {'{variable}'} substitution) |
method | "GET" \| "POST" \| "PUT" \| "PATCH" \| "DELETE" | — | HTTP method |
headers | object | {} | Request headers |
body | object | {} | Request body — {'{variable}'} for template vars, <<field>> for hook fields |
timeout | number | 10 | Request timeout in seconds |
max_retries | number | 3 | Maximum retry attempts |
Best practices
Function Naming Conventions
Use descriptive, action-oriented names that reflect what happened: appointment_confirmed, wants_to_reschedule, email_collected.
- Descriptions over names: The LLM relies primarily on the
descriptionfield - One function per outcome: Each distinct outcome should have its own function
- Use enums for constrained values: Leverage
enumin JSON Schema - Mark required fields: Only mark parameters as
requiredif truly needed - Hook ordering: Place
update_outcome_in_databasefirst, thensend_http_request