Tools & function calling
Tools & function calling
Tools let an agent take actions mid‑conversation — look up an order, book a slot, transfer the call, or hang up — by calling functions you define. Telenow exposes the tools to the language model in the standard OpenAI function‑calling format, runs the call when the model invokes it, and feeds the result back so the agent can keep talking.
How tools are attached to an agent
Tools live in the agent's metadata.tools array — a list of tool specs you set when creating or updating an agent:
{
"name": "Support Bot",
"llmProvider": "openai",
"llmModel": "gpt-4o-mini",
"sttProvider": "deepgram",
"ttsProvider": "elevenlabs",
"ttsVoice": "rachel",
"sessionConfig": {},
"metadata": {
"tools": [ /* tool specs (below) */ ]
}
}
In the dashboard, the agent builder's Tools step writes the same array.
Tools apply to the standard LLM providers (OpenAI, Groq, Anthropic, Custom LLM). The
customapiprovider runs its own agentic loop, so it doesn't use Telenow tools — you define tools inside your own endpoint, hang up with acall_endSSE event, and transfer viaPOST /api/sessions/{id}/transfer. See Custom LLM & API.
Tool spec
Each tool is an object:
| Field | Type | Notes |
|---|---|---|
name | string | Required. Function name the model calls |
description | string | What the tool does — the model uses this to decide when to call it |
parameters | object | JSON‑Schema for the arguments (type, properties, required) |
endpoint | string | For HTTP tools: your https:// webhook (see below). Empty for native tools |
bearer | string | Optional bearer token sent to your endpoint |
kind | string | "http" (default) or a native kind: "transfer", "end_call"; "mcp" for MCP‑sourced tools; "connector" for workspace‑integration actions |
config | object | Native‑tool / MCP / connector configuration (see below) |
request | object | For request‑template tools: an arbitrary HTTP request ({method, url, headers, body}) with {placeholder} tokens. Used instead of endpoint — see below |
handoff | string | Optional line spoken while the tool runs ("Let me check that for you…"). For parallel tool calls the lines are joined with "and also". Native tools speak their own line, so this is for HTTP / curl / MCP / connector tools |
timeoutSecs | number | Optional total‑request timeout for this tool, clamped to 1–30 (default 15). Raise it for slow back‑ends (a Make scenario behind a webhook response, a Zapier MCP call); past 30 s the conversation is dead, so that's the ceiling. Applies to HTTP, request‑template, connector, and MCP tools |
requiresConfirmation | boolean | Optional. When true, the agent reads the action back and waits for a verbal yes before running it (see below). Defaults to false, but money‑moving connector actions (payment.*) are always confirmed regardless of this flag. Ignored for native transfer/end_call |
HTTP tools (your webhook)
When kind is omitted or "http", the agent's call is forwarded to your server. Telenow POSTs to your endpoint:
POST https://your-server.com/voice-tools/lookup_order
{ "name": "lookup_order", "arguments": { "orderId": "A-1234" } }
Your server returns a JSON body; Telenow hands the (stringified) result back to the model. Example definition:
{
"name": "lookup_order",
"description": "Look up the status of a customer order by id.",
"parameters": {
"type": "object",
"properties": { "orderId": { "type": "string", "description": "Order id, e.g. A-1234" } },
"required": ["orderId"]
},
"endpoint": "https://your-server.com/voice-tools/lookup_order",
"bearer": "optional-shared-secret"
}
Security: tool endpoints must be https:// (plain HTTP, file:, data:, and internal/loopback addresses are rejected — an SSRF guard). Respond quickly; the caller is waiting on the line.
Request-template tools (import from curl)
When you need an arbitrary request (a specific method, custom headers, a templated body) rather than the fixed POST {name, arguments} above, give the tool a request template instead of an endpoint. In the agent builder, Tools → Import from curl parses a pasted curl into this shape; use {placeholder} tokens for the values the model fills at call time (they become the tool's parameters).
{
"name": "create_ticket",
"description": "Open a support ticket.",
"parameters": {
"type": "object",
"properties": { "subject": { "type": "string" }, "priority": { "type": "string" } },
"required": ["subject"]
},
"request": {
"method": "POST",
"url": "https://your-server.com/tickets",
"headers": { "Content-Type": "application/json", "Authorization": "Bearer ..." },
"body": "{\"subject\":\"{subject}\",\"priority\":\"{priority}\"}"
}
}
At call time Telenow substitutes the {placeholder} tokens (in the url, header values, and body) with the model's arguments, then sends the request. The same https://-only SSRF guard applies to the resolved URL. The response (JSON, or raw text) is handed back to the model. Single braces are placeholders; {{/}} are literal braces, so JSON bodies only substitute real argument names.
Placeholder filters
A placeholder can carry one filter — {name|filter} — that transforms the value before it's inserted. Handy when a back‑end is fussy about formatting (e.g. a WhatsApp gateway that wants the number with no +):
| Filter | What it does | Example |
|---|---|---|
digits | Keep only the digits (strips +, spaces, dashes) | `{to |
trim | Strip leading/trailing whitespace | `{name |
lower | Lower‑case the value | |
upper | Upper‑case the value | `{code |
a1 | Sheet/tab name escaping for spreadsheet A1 ranges — strips a surrounding quote pair and doubles embedded single quotes (Sarah's leads → Sarah''s leads) | `'{sheet |
An unknown filter leaves the whole {name|filter} run untouched (no silent data loss). Unrecognised placeholder runs (empty, invalid characters, multi‑line) are also left as‑is. These same filters work in connector bindings (see Integrations).
MCP servers (Model Context Protocol)
Attach tools from a remote MCP server instead of (or alongside) your own webhooks. In the agent builder, Tools → Connect MCP server: enter the server URL (+ an optional auth token), Verify to list its tools, then pick the ones to add. Each selected tool is stored on the agent as a kind: "mcp" entry that carries the server reference:
{
"name": "search_docs",
"description": "Search the knowledge base.",
"parameters": { "type": "object", "properties": { "query": { "type": "string" } } },
"kind": "mcp",
"config": { "url": "https://mcp.example.com/mcp", "token": "optional", "mcpName": "search_docs" }
}
At call time Telenow connects to the server over Streamable HTTP (JSON-RPC initialize → tools/call) and hands the tool's result back to the model. Only remote servers reachable over https:// are supported (same SSRF guard); stdio / locally-spawned servers are not. The token, when set, is sent as a bearer credential.
Connector tools (workspace integrations)
Ready‑made actions — send a WhatsApp message, create a CRM lead or contact, append a spreadsheet row, create a payment or booking link — executed through a vendor account connected once under Workplace → Integrations (full catalog + setup in Integrations). In the builder these appear as one‑click tool cards for every action your connections offer; the stored spec references the connection, never the credentials:
{
"name": "log_to_sheet",
"description": "Save the caller's details as a new row in the Google Sheet.",
"kind": "connector",
"config": {
"connectionId": "…",
"capability": "sheets.append",
"settings": { "spreadsheet_id": "…", "sheet_name": "Sheet1" },
"columns": [ { "key": "Name", "label": "Name", "param": "name" } ]
}
}
settings are per‑tool binding values — which spreadsheet / base / form this tool targets — and columns are the fields discovered from that target via Load fields; each becomes a string parameter the model fills. The connector catalog and setup walkthrough are in Integrations and Building agents.
Confirm before executing
For tools that do something irreversible — send money, message a customer, write a record — you usually want the agent to read the action back and get a verbal "yes" before it runs. Set requiresConfirmation: true on the tool (a checkbox in the builder).
How it works: the first time the model calls a confirmation‑gated tool, Telenow does not execute it. Instead it returns a "read this back and ask the caller to confirm" result, so the agent says something like "I'll send a payment link for $6 to +1… — shall I go ahead?" and only runs the tool on a later turn once the caller agrees. The read‑back summary is tailored for known actions (payment links quote the amount, WhatsApp quotes the recipient) and generic otherwise — and it only ever uses the tool's arguments, never secrets.
Payments are always confirmed. Any connector tool whose capability starts with
payment.(e.g.payment.link) is gated even if you forget to set the flag — defense in depth, so a careless config can't ship an unconfirmed money‑moving tool. Nativetransfer/end_callare explicit by nature and are never gated.
Native tools: transfer & end_call
Native tools are handled inside Telenow — no endpoint, no outbound POST. Set kind and config:
transfer
Hand the live call to a human or another number. The model picks a destination by label; Telenow resolves it to an E.164 number and warm‑transfers. Works on phone calls on every carrier (Plivo, Twilio, Vobiz, Exotel, Vonage, and SIP trunks — see Telephony providers) and on web (browser) calls — for a web call Telenow dials the destination over the phone network from the agent's bound number and bridges the browser audio to that call, so the agent needs a phone number assigned on the Numbers page. Tata Tele Smartflo is the exception: a transfer on a live phone call is refused, because Tata offers no way to redirect or bridge a call already on a voice-streaming leg. A transfer from a web call whose agent is bound to a Smartflo number does work — there Telenow places a fresh outbound call to the human rather than redirecting an existing one.
{
"name": "transfer_to_team",
"description": "Transfer the caller to the right team when they ask for a human.",
"kind": "transfer",
"config": {
"destinations": [
{ "label": "billing", "number": "+14155550111" },
{ "label": "sales", "number": "+14155550112" }
],
"message": "Sure, connecting you now."
}
}
end_call
Let the agent end the call gracefully with an optional spoken goodbye.
{ "name": "hang_up", "description": "End the call when the conversation is complete.", "kind": "end_call", "config": { "message": "Thanks for calling — goodbye!" } }
Voicemail is not a tool. Leaving a voicemail is automatic via answering‑machine detection (AMD): set the agent's voicemail message (Identity → Call handling), enable AMD per‑call or per‑campaign, and Telenow plays it when a machine answers. There's no LLM‑callable voicemail tool. See Leaving a voicemail message.
You can also transfer a call programmatically (outside the model's control) with
POST /api/sessions/{id}/transfer— see Sessions & calls.
Caller identity (validate the caller)
When a tool needs to validate or look up who's on the call — "is this really account 42?", "fetch this caller's open orders" — you must not rely on the model to provide the phone number or account id. The model can hallucinate a value, and a caller can simply lie ("I'm calling about account 99"). So Telenow can attach a trusted, system‑resolved caller identity to every tool call. It comes from the carrier or from your own API request — never from the model — so your endpoint can treat it as authoritative.
It's off by default (caller phone numbers shouldn't leak to third‑party tool endpoints unless you intend it). Turn it on per agent.
1. Enable it on the agent
In the agent builder, Tools → "Send caller identity to tools". Optionally set an identifier label (e.g. "Account number") — it's just a hint shown on the test panel and Publish page. This saves metadata.callerIdentity = { "enabled": true, "label": "Account number" } on the agent.
2. What gets resolved (per call type)
| Call | caller.number | caller.identifier | caller.channel |
|---|---|---|---|
Outbound (initiate-call) / campaign | the dialed number (the customer) | the identifier you pass (or a campaign identifier column) | phone_outbound |
| Inbound | the caller ID / ANI — may be absent if the caller withholds it | — (a cold inbound call has no app‑level id) | phone_inbound |
Web / API (init-web-call) / dashboard test | — (web calls have no phone number) | the identifier you pass | web |
caller.session_id (the call's id) is always included. Empty values are omitted.
3. Pass an identifier when you start the call
Both call‑start endpoints accept an optional identifier (the dialed number is captured automatically for phone calls, so you only pass the identifier):
# Outbound phone call
curl -X POST https://api.telenow.ai/api/sessions/initiate-call \
-H "Authorization: Bearer <YOUR_API_KEY>" -H "Content-Type: application/json" \
-d '{ "agentId": "…", "mobileNumber": "+14155550123", "identifier": "acct_42" }'
# Web call (browser voice) — call this from YOUR server
curl -X POST https://api.telenow.ai/api/sessions/init-web-call \
-H "X-API-Key: <YOUR_API_KEY>" -H "Content-Type: application/json" \
-d '{ "agentId": "…", "identifier": "acct_42" }'
You can send variables and identifier together. The Agent → Publish tab generates these snippets for you, pre‑filled, whenever the agent has caller identity enabled. On the dashboard test panel an identifier field appears (labelled with your hint) so you can try it without writing code.
The anonymous public widget / share link (
/p/…) does not send an identifier — anyone could set it, so it wouldn't be trustworthy. Use the server‑sideinit-web-call(with your API key) when you need a verified identifier.
4. How your tools receive it
Only non‑native tools get it, and only when enabled:
-
HTTP tools — a
callerobject is added at the top level, next toarguments:POST https://your-server.com/voice-tools/lookup_order { "name": "lookup_order", "arguments": { "orderId": "A-1234" }, "caller": { "number": "+14155550111", "identifier": "acct_42", "channel": "phone_inbound", "session_id": "8f3c…" } }Your endpoint validates against
caller, not against anything the model said. For example (Express):app.post('/voice-tools/lookup_order', (req, res) => { const { arguments: args, caller } = req.body; // Trust caller.identifier / caller.number — NOT a value from args. const account = lookupAccount(caller?.identifier ?? caller?.number); if (!account) return res.json({ error: 'caller not recognised' }); res.json({ status: account.orders[args.orderId] ?? 'not found' }); }); -
Request‑template (curl) tools — use the reserved placeholders anywhere in the url, header values, or body. They're filled from the trusted identity and override any model argument of the same name, so they can't be spoofed:
Placeholder Value {caller_number}the trusted phone number — a phone leg only {caller_identifier}the trusted identifier; on a WhatsApp or Instagram thread this is the contact's phone number {caller_channel}phone_inbound/phone_outbound/web/whatsapp/instagram/chat{session_id}the call id A WhatsApp, Instagram or web‑chat session has no
{caller_number}— the contact is resolved into{caller_identifier}instead. Binding a "Send WhatsApp message" tool'stoon a chat agent therefore meanscaller_identifier, notcaller_number.{ "method": "GET", "url": "https://your-crm.com/customers/{caller_identifier}", "headers": { "X-Caller": "{caller_number}" } } -
MCP tools — the
callerobject is merged into the tool'sarguments(MCP servers only seeparams.arguments):{ "name": "lookup_order", "arguments": { "orderId": "A-1234", "caller": { "number": "+14155550111", "identifier": "acct_42", "channel": "phone_inbound", "session_id": "8f3c…" } } }
Inbound calls — a note on trust
On an inbound cold call there's no app‑level identifier, so the only trusted key is the caller ID (caller.number / ANI), which may be absent if withheld — handle that case. If you need a stronger key (e.g. an account number), have the agent ask for it; that value arrives as a normal tool argument, which is fine for looking something up but is weaker than the ANI for authentication (the caller could give any number). Use caller.number for trust, an asked‑for argument for convenience.
Observing tool calls
Every tool invocation is logged and emitted as a tool.invoked webhook with the name, arguments, result, status, and latency — useful for debugging and analytics.
Tips
- Keep tools fast. The caller is on the line. Telenow's per‑tool timeout defaults to 15 s (raise with
timeoutSecs, max 30) — and the agent speaks thehandoffline while it waits, so set a friendly one ("Let me check that…"). - Write descriptions for the model, not for humans. The
descriptionis how the model decides when to call the tool. Be specific about what it does and when to use it. - Mark required arguments in the
parametersschema'srequiredarray so the model collects them before calling. - Use confirmation for anything irreversible — set
requiresConfirmation: true(payments are forced on regardless). - Validate the caller with
caller, not arguments. A caller can claim any account number. Trustcaller.number/caller.identifier; treat asked‑for values as convenience only.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Tool never fires | description too vague, or a required argument the model can't fill. Make the description explicit; check tool.invoked webhooks |
tool endpoint rejected / tool url rejected | The URL isn't https://, or it resolves to a loopback / private / link‑local / cloud‑metadata address (SSRF guard). Use a public https:// host |
request failed (4xx/5xx) | Your endpoint returned a non‑2xx status — the trimmed body is surfaced to the model and the tool log so you can see why |
tool response exceeded 1 MB cap | Return a smaller JSON payload; responses are capped at 1 MB |
| Tool times out | Your back‑end is slower than timeoutSecs (default 15, max 30). Speed it up or raise the limit |
| Caller identity missing in the payload | It's off by default — enable Tools → "Send caller identity to tools" on the agent, and pass an identifier when you start the call. Inbound cold calls may have no caller.number if the ANI is withheld |
| Confirmation never asked | requiresConfirmation not set (and the capability isn't payment.*). Native transfer/end_call are never gated |