Skip to main content

What are Custom Functions?

Custom functions let your Voice AI agent call your own APIs during live conversations. Check order status, update a CRM, book appointments, or integrate with any system - all while on a call.
Bolna custom functions follow the OpenAI function calling specification. If you’ve used OpenAI function calling before, you’ll feel right at home.
Bolna custom function configuration modal with dark code editor showing JSON schema with name test, description, pre_call_message, parameters with startTime and endTime, key custom_task, and value with method GET, url, api_token, and headers

How Custom Functions Work

1

LLM Reads Function Definition

The LLM sees your function’s name and description to understand what the function does
2

Decides When to Call

Based on conversation context, the LLM decides if this function should be triggered
3

Extracts Parameters

The LLM collects required parameter values from the conversation with the caller
4

Bolna Executes API Call

Bolna makes the HTTP request to your API endpoint with the extracted parameters
5

Response Feeds Back

The API response is returned to the LLM, which continues the conversation naturally
The description is everything. The LLM relies heavily on your description to know when to call the function. A vague description = unreliable triggering.

Creating a Custom Function

You can create a custom function in two ways from the Tools Tab:
Bolna Add a Custom Tool section with Custom Function card showing Write manually button and Generate from cURL button side by side
Click Write manually to open the JSON editor and define the function schema from scratch. This gives you full control over every field.
Bolna custom function configuration modal with dark code editor, JSON schema with name test, description, pre_call_message, parameters, key custom_task, and value object with method, url, api_token, and headers
The fields name, description, key, and method are mandatory. The key must always be set to "custom_task".

Complete Schema Structure

Every custom function has two parts: Function Definition (tells LLM what it does) and API Configuration (tells Bolna how to call your API).
{
  "name": "function_name",
  "description": "Detailed description of when to call this function",
  "pre_call_message": "What agent says while the API is being called",
  "parameters": {
    "type": "object",
    "properties": {
      "param_name": {
        "type": "string",
        "description": "What this parameter represents"
      }
    },
    "required": ["param_name"]
  },
  "key": "custom_task",
  "value": {
    "method": "GET",
    "param": {
      "param_name": "%(param_name)s"
    },
    "url": "https://your-api.com/endpoint",
    "api_token": "Bearer your_token",
    "headers": {}
  }
}
Mandatory: The field "key": "custom_task" must be present exactly as shown. Do not modify this value.

Schema Reference

Parts 1-2 follow the OpenAI function calling specification - they define what the function does. Parts 3-5 are Bolna extensions that define how to execute the API call automatically.

1. Function Definition

These fields tell the LLM about your function:
FieldRequiredDescription
nameYesUnique function identifier. Use snake_case (e.g., get_order_status)
descriptionYesTells the LLM when to trigger this function. Be specific and detailed.
parametersYesDefines what information to collect from the caller

Examples

The following examples use illustrative endpoints and credentials to demonstrate the schema structure. Replace the URLs, tokens, and parameter values with your own API details when implementing.
A customer calls and asks “Where is my order?” The agent collects the order ID and fetches the status from your backend.What the agent says: “Let me check the status of your order.”What Bolna sends:
curl --location 'https://api.yourstore.com/orders?order_id=ORD-78234' \
--header 'Authorization: Bearer sk_live_abc123'
Function schema:
{
  "name": "check_order_status",
  "description": "Use this function when the customer asks about their order status, delivery update, shipping progress, or tracking information. The customer must provide their order ID.",
  "pre_call_message": "Let me check the status of your order.",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "The customer's order ID. Usually starts with ORD- followed by numbers, e.g., ORD-78234."
      }
    },
    "required": ["order_id"]
  },
  "key": "custom_task",
  "value": {
    "method": "GET",
    "param": {
      "order_id": "%(order_id)s"
    },
    "url": "https://api.yourstore.com/orders",
    "api_token": "Bearer sk_live_abc123",
    "headers": {}
  }
}
Example API response the LLM receives:
{
  "order_id": "ORD-78234",
  "status": "shipped",
  "estimated_delivery": "March 15, 2026",
  "tracking_number": "1Z999AA10123456784"
}
The agent would then say something like: “Your order ORD-78234 has been shipped and is expected to arrive by March 15th. Your tracking number is 1Z999AA10123456784.”
A customer calls and asks “What’s my current balance?” The agent verifies their identity using their registered phone number and account ID, then fetches the balance.What the agent says: “Let me pull up your account details.”What Bolna sends:
curl --location 'https://api.yourbank.com/accounts/balance?account_id=ACC-991042&phone=%2B919876543210' \
--header 'Authorization: Bearer fin_api_key_001' \
--header 'X-Request-Source: voice-agent'
Function schema:
{
  "name": "get_account_balance",
  "description": "Use this function when the customer asks about their account balance, available credit, remaining amount, or account summary. Requires the customer's account ID and phone number for verification.",
  "pre_call_message": "Let me pull up your account details.",
  "parameters": {
    "type": "object",
    "properties": {
      "account_id": {
        "type": "string",
        "description": "The customer's account ID, usually starts with ACC- followed by numbers."
      },
      "phone": {
        "type": "string",
        "description": "The customer's registered phone number for identity verification."
      }
    },
    "required": ["account_id", "phone"]
  },
  "key": "custom_task",
  "value": {
    "method": "GET",
    "param": {
      "account_id": "%(account_id)s",
      "phone": "%(phone)s"
    },
    "url": "https://api.yourbank.com/accounts/balance",
    "api_token": "Bearer fin_api_key_001",
    "headers": {
      "X-Request-Source": "voice-agent"
    }
  }
}
Example API response the LLM receives:
{
  "account_id": "ACC-991042",
  "name": "Rahul Verma",
  "current_balance": 24500.75,
  "currency": "INR",
  "last_transaction": "2026-03-12"
}
The agent would then say something like: “Your current account balance is Rs. 24,500.75. Your last transaction was on March 12th.”
The phone parameter can be auto-injected using the {from_number} context variable, so the caller does not need to repeat their number.

Using Variables and Dynamic Context

Context variables defined in your agent prompt are automatically substituted into custom functions. The LLM won’t ask the caller for these values - they’re already available.

Auto-Injected System Variables

VariableDescription
{agent_id}The id of the agent
{call_sid}Unique id of the phone call (from Twilio, Plivo, etc.)
{from_number}Phone number that initiated the call
{to_number}Phone number that received the call

How Variable Substitution Works

You are agent Sam speaking to {customer_name}.
The agent ID is "{agent_id}".
The call ID is "{call_sid}".
The customer's phone is "{to_number}".
ParameterDefined in Prompt?Result
to_numberYes (system)Auto-substituted
agent_idYes (system)Auto-substituted
customer_nameYes (passed via call)Auto-substituted
customer_addressNoLLM will collect from caller

Pre-call Webhooks

A custom function can fire a pre-call webhook — a notification sent to a URL of your choice before the tool’s main API call runs. A common use case is a tool that transfers the call, where your system needs to receive the transfer reason before the transfer happens.
The pre-call webhook is fire-and-forget. A slow or failing webhook endpoint never blocks or fails the function call itself.

How It Works

1

LLM decides to call the tool

The LLM produces the arguments for your custom function as usual.
2

Bolna fires the pre-call webhook

If the tool has a pre_call_webhook_param configured, Bolna first POSTs the pre-call webhook to the resolved URL.
3

The main API call runs

The tool’s main function call then executes exactly as it normally would.

Configuring the Webhook Body

pre_call_webhook_param is a JSON template that is completely independent from the tool’s main param. You can reference any argument the LLM produced for the tool using the same %(field)s substitution syntax used by param. Static values are passed through as-is.
{
  "name": "transfer_support",
  "description": "Transfers the call to a support agent when the caller asks for a human or has an issue the agent cannot resolve.",
  "parameters": {
    "type": "object",
    "properties": {
      "reason": {
        "type": "string",
        "description": "Why the caller wants a transfer"
      }
    },
    "required": ["reason"]
  },
  "key": "custom_task",
  "value": {
    "method": "POST",
    "url": "https://your-api.com/transfer",
    "param": {
      "reason": "%(reason)s"
    },
    "pre_call_webhook_url": "https://your-api.com/pre-transfer-hook",
    "pre_call_webhook_param": {
      "transfer_reason": "%(reason)s",
      "channel": "voice"
    }
  }
}
In the example above, %(reason)s is replaced with the LLM’s argument for this tool call, while "channel": "voice" is a static value passed through unchanged.

What Your Endpoint Receives

The webhook body is the same execution record you receive on the post-call execution webhook (execution id, agent id, telephony details, status, etc.), merged with the fields from your pre_call_webhook_param:
{
  "id": "<execution_id>",
  "agent_id": "<agent_id>",
  "status": "in-progress",
  "telephony_data": { "...": "..." },
  "...": "...",

  "transfer_reason": "customer asked for billing",
  "channel": "voice"
}
Because the call is still in progress when the pre-call webhook fires, fields that are only finalized at call end (transcript, cost, summary) won’t be complete yet.

URL Resolution Rules

pre_call_webhook_parampre_call_webhook_urlResult
SetSetWebhook sent to the tool’s pre_call_webhook_url.
SetNot setWebhook sent to the agent-level Webhook URL (the same URL used for post-call execution webhooks).
Not setSet or not setNo pre-call webhook fires.
pre_call_webhook_param is the master switch. If it is not set, no pre-call webhook fires — even if pre_call_webhook_url is configured. The agent’s normal post-call webhook is unaffected.
Agent-level URL fallback: when pre_call_webhook_param is set without a pre_call_webhook_url, the pre-call webhook is sent to your agent’s configured Webhook URL. If you already use that endpoint for post-call execution webhooks, it will now also receive pre-call webhooks. Distinguish them by the in-progress status and the extra fields from your pre_call_webhook_param.

UI Configuration

In the agent dashboard, the custom tool configuration (on the Tools Tab) has two optional inputs that map to these fields:
  • Pre-call webhook URL — the endpoint to notify (pre_call_webhook_url).
  • Pre-call webhook parameters — the JSON body template with %(field)s substitution (pre_call_webhook_param).
Both save with the tool and round-trip on edit.

Best Practices

Write Detailed Descriptions

Include synonyms and variations: “Use when customer asks about order status, shipping, delivery, or tracking”

Always Add Pre-call Message

Set a friendly message like “Let me check that for you” so callers know to wait

Use Required Fields Wisely

Only mark parameters as required if the function truly cannot work without them

Test APIs First

Verify your API works with tools like curl or Postman before adding to Bolna

Leverage Auto-Injection

Put known data in the agent prompt to avoid asking callers for information you already have

Match Names Exactly

Parameter names in properties and param must be identical (case-sensitive)

Troubleshooting

Cause: Description doesn’t match what the caller is saying.Fix: Make your description more comprehensive. Include synonyms and example phrases.
Cause: Incorrect URL, authentication, or headers.Fix: Test your API with curl or Postman first. Verify api_token format and required headers.
Cause: Typo or format specifier mismatch.Fix: Ensure parameter names match exactly between properties and param. Use %(name)s format.
Cause: Variable not defined in agent prompt.Fix: Add the variable to your agent prompt using {variable_name} syntax.

Next Steps

Context Variables

Learn about dynamic variable injection

Transfer Calls

Route calls to human agents

Calendar Integration

Book appointments via Cal.com

Using Webhooks

Receive execution data, including pre-call webhooks

Agent API

Programmatic function configuration