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 customapi provider runs its own agentic loop, so it doesn't use Telenow tools — you define tools inside your own endpoint, hang up with a call_end SSE event, and transfer via POST /api/sessions/{id}/transfer. See Custom LLM & API.

Tool spec

Each tool is an object:

FieldTypeNotes
namestringRequired. Function name the model calls
descriptionstringWhat the tool does — the model uses this to decide when to call it
parametersobjectJSON‑Schema for the arguments (type, properties, required)
endpointstringFor HTTP tools: your https:// webhook (see below). Empty for native tools
bearerstringOptional bearer token sent to your endpoint
kindstring"http" (default) or a native kind: "transfer", "end_call"; "mcp" for MCP‑sourced tools; "connector" for workspace‑integration actions
configobjectNative‑tool / MCP / connector configuration (see below)
requestobjectFor request‑template tools: an arbitrary HTTP request ({method, url, headers, body}) with {placeholder} tokens. Used instead of endpoint — see below
handoffstringOptional 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
timeoutSecsnumberOptional 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
requiresConfirmationbooleanOptional. 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 +):

FilterWhat it doesExample
digitsKeep only the digits (strips +, spaces, dashes)`{to
trimStrip leading/trailing whitespace`{name
lowerLower‑case the value`{email
upperUpper‑case the value`{code
a1Sheet/tab name escaping for spreadsheet A1 ranges — strips a surrounding quote pair and doubles embedded single quotes (Sarah's leadsSarah''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 initializetools/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. Native transfer/end_call are 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)

Callcaller.numbercaller.identifiercaller.channel
Outbound (initiate-call) / campaignthe dialed number (the customer)the identifier you pass (or a campaign identifier column)phone_outbound
Inboundthe 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 passweb

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‑side init-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 caller object is added at the top level, next to arguments:

    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:

    PlaceholderValue
    {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's to on a chat agent therefore means caller_identifier, not caller_number.

    { "method": "GET",
      "url": "https://your-crm.com/customers/{caller_identifier}",
      "headers": { "X-Caller": "{caller_number}" } }
    
  • MCP tools — the caller object is merged into the tool's arguments (MCP servers only see params.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 the handoff line while it waits, so set a friendly one ("Let me check that…").
  • Write descriptions for the model, not for humans. The description is 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 parameters schema's required array 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. Trust caller.number / caller.identifier; treat asked‑for values as convenience only.

Troubleshooting

SymptomLikely cause
Tool never firesdescription too vague, or a required argument the model can't fill. Make the description explicit; check tool.invoked webhooks
tool endpoint rejected / tool url rejectedThe 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 capReturn a smaller JSON payload; responses are capped at 1 MB
Tool times outYour back‑end is slower than timeoutSecs (default 15, max 30). Speed it up or raise the limit
Caller identity missing in the payloadIt'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 askedrequiresConfirmation not set (and the capability isn't payment.*). Native transfer/end_call are never gated