Concept

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.

LLM decides to call function
Extract arguments from conversation
Fire hooks (async, non-blocking)
Execute transition_to (if set)
Enter new node

FlowFunction fields

FieldTypeDescription
namestringUnique function name within the node
descriptionstringTells the LLM when to call this function
propertiesJSON SchemaParameter definitions the LLM should extract
requiredstring[]Required parameter names
transition_toOptional[string]Node to transition to. null = stay on current node
hooksList[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

  1. The LLM receives node task messages plus available functions (node + global)
  2. Based on conversation, the LLM decides to call a function and extracts arguments
  3. Arguments are validated against properties and required
  4. Hooks execute asynchronously (non-blocking)
  5. If transition_to is set, conversation moves to that node

Function examples

navigate-to-node.json
json
{
  "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

collect-email.json
json
{
  "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

FieldTypeDescription
namestringHook identifier — determines which handler runs
expected_fieldsDict[string, FieldConfig]Map of field names to source config
http_requestOptional[HttpRequestConfig]HTTP config (required for send_http_request)

Available hook types

Hook NameDescriptionRequires http_request
update_outcome_in_databaseUpdates the lead’s outcome and metadataNo
send_http_requestFires an HTTP request to an external serviceYes
set_transfer_flagSets a Redis context flag for warm transferNo

Field source types

SourceValue FieldDescription
staticRequiredHardcoded value. Supports {'{variable}'} substitution
llmNot usedValue from the LLM function call arguments
computedRequiredDynamic runtime value (e.g. utc_now_minus_hours:1)
field-sources.json
json
{
  "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

FieldTypeDefaultDescription
urlstringTarget URL (supports {'{variable}'} substitution)
method"GET" \| "POST" \| "PUT" \| "PATCH" \| "DELETE"HTTP method
headersobject{}Request headers
bodyobject{}Request body — {'{variable}'} for template vars, <<field>> for hook fields
timeoutnumber10Request timeout in seconds
max_retriesnumber3Maximum 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 description field
  • One function per outcome: Each distinct outcome should have its own function
  • Use enums for constrained values: Leverage enum in JSON Schema
  • Mark required fields: Only mark parameters as required if truly needed
  • Hook ordering: Place update_outcome_in_database first, then send_http_request
Was this helpful?