# Tools (/docs/agents/voice-agents/tools)

> For the complete documentation index, see [llms.txt](https://docs.tandemcx.ai/llms.txt). Any page on this site can be fetched as Markdown by appending `.md` to its URL.

Tools are the actions an agent can take mid-conversation. Without them an agent can only
talk; with them it can look up an account, book an appointment, create a ticket, or check
an order — and speak the result back to the caller.

You add tools on the **Custom Tools** row, in the **Tools & Skills** group on the editor's
**Conversation** step. The agent decides for itself when to use one, based on
the tool's description and what the caller has said. You don't script when a tool fires; you
describe what it's for.

## The six kinds of tool

Choose a kind in each tool row's first dropdown.

**Workflow (Invoke)** — runs one of your [workflows](/docs/workflows) and returns its result to
the agent, which then speaks it. The caller stays with the agent throughout. Use this when the
action already exists as a workflow, or when it needs several steps, integrations, or business
logic you'd rather build on the canvas than describe to a model.

**Workflow (Handoff)** — transfers the live call to the workflow. The agent stops speaking and
resumes when the workflow finishes. Use this when the workflow needs to talk to the caller
itself: collect a card number, play a menu, queue to a person. The two workflow kinds look
identical in the picker, and this is the difference that matters at call time.

In a [website chat](#handoffs-in-website-chat) the handoff keeps the visitor in the same panel. In
[tests](/docs/agents/voice-agents/tests) the agent can use it, and the test ends without running
the workflow.

**Integration** — calls one of your connected integrations. For a **generic** integration you
define the HTTP request yourself; for a first-class one such as **Zendesk**, **Slack**, or
**Meridian** you pick an action from a list instead and Tandem builds the request. See
[Add an integration tool](#add-an-integration-tool).

**MCP** — connects the agent to an MCP integration and exposes the tools that server offers.
Unlike the other kinds you don't define a name, description, or parameters: the server
publishes its own tools and the agent discovers them when the call starts. Whichever tools
you've enabled on that integration are the ones the agent can use.

**Agent** — consults one of your other agents and speaks with the caller once it answers. Use
this to split a broad agent into focused specialists (a billing agent, a scheduling agent) that
a single caller-facing agent can call on without duplicating their prompts or tools.

**Node** — runs a single workflow node directly, without wrapping it in a workflow. Use this
for a one-step action (send an email, send a text, log an event) that doesn't need a canvas
of its own. See [Add a node tool](#add-a-node-tool) below.

## Add a workflow tool

<Steps>
<Step>
### Add the row

Click **Add Tool**. New rows start as **Workflow (Invoke)**.

</Step>

<Step>
### Pick the workflow

Choose it from the second dropdown, which lists your workflows.

</Step>

<Step>
### Name the tool

The **Name** is what the model calls, not what the caller hears. Use a verb phrase in
snake_case: `look_up_order`, `book_appointment`, `create_ticket`.

Letters, numbers, hyphens, and underscores only, up to 64 characters.

</Step>

<Step>
### Describe it

The **Description** is how the agent decides whether this tool is the right one. Write it as
the situation it's for, not as a restatement of the name:

```
Look up the status of a customer's order by order number.
Use this when the caller asks where their order is or when it will arrive.
```

</Step>

<Step>
### Add the parameters

Click **Add Parameter** once per value the tool needs, and fill in the row — see
[Parameters](#parameters) below. A workflow tool's parameters arrive as the workflow's starting
variables, named exactly as you name them here.

A tool that needs no values needs no parameters. Leave the list empty.

</Step>

<Step>
### Save

Click **Save Configuration**. The tool is available on the agent's next call. On an agent that is
on [versioning](/docs/agents/voice-agents/versioning), you'll be editing a draft — the button reads
**Save as New Draft** if you started from the published version — and the tool reaches callers when
you publish that draft.

</Step>
</Steps>

## Parameters

Parameters are declared one row at a time, not written as JSON. Each row is a small form:

| Field           | What it is                                                                                                                                                          |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name**        | The wire name — the `{token}` in a request path, the query key, the body key, or the header name. For a **Model** parameter it's also the name the model fills.     |
| **Type**        | `string`, `number`, `integer`, or `boolean` — or **Object**, which holds nested parameters instead of a value. See [Nested request bodies](#nested-request-bodies). |
| **Location**    | Integration tools only: `path`, `query`, `body`, or `header`. See [Add an integration tool](#add-an-integration-tool).                                              |
| **Value from**  | Where the value comes from: **Model**, **Fixed**, or **Call** (below).                                                                                              |
| **Description** | **Model** parameters only, and required — it's how the model knows what to put there.                                                                               |
| **Fixed value** | **Fixed** parameters only.                                                                                                                                          |
| **Call value**  | **Call** parameters only.                                                                                                                                           |
| **Required**    | Whether the model must supply it. A `path` parameter is always required.                                                                                            |

**Value from** is the field worth understanding, because it decides what the model can see:

- **Model** — the agent fills it in from the conversation. This is the only source the model is
  told about, and the only one it can influence.
- **Fixed** — a value you type, sent on every call. An API version, a tenant code, a
  content type. The model never sees it and can't change it.
- **Call** — a value read from the live call. Pick one of `call.ani` (the caller's number),
  `call.id`, `agent.id`, or `org.id`. Nothing else is available, and the model can't
  substitute one.

Fixed and Call parameters are the reason you rarely need to trust the model with anything but
the caller's own words: a lookup keyed on `call.ani` can't be pointed at somebody else's
account by a caller who asks nicely.

Expand **What the model sees** under the parameter list to check your work. It shows the exact
schema the agent is given — only **Model** parameters appear in it, with their descriptions and
which are required. It's generated by the same code the live call uses, so it can't drift from
what the agent actually receives.

<Callout type="info">
    A **Workflow (Handoff)** tool takes **Model** parameters only. Its arguments ride the transfer
    itself, so a Fixed or Call value would save and then never reach the workflow. The editor
    doesn't offer them.
</Callout>

### What the agent gets back from a handoff

When the workflow finishes, the caller returns to the same agent, with the conversation from
before the transfer still in place. The agent speaks first and does not re-greet: it picks up
from the workflow's result, so the caller hears an answer rather than a second hello.

To hand a value back, end the workflow with a **Response** node. Its JSON object becomes the
tool's result, and the agent can read the values out of it:

```json
{ "balance": "$42.00", "dueDate": "2026-10-01" }
```

Without a **Response** node the agent still resumes and still knows the workflow completed. It
just has no data to speak, so it opens by asking what the caller needs next. A workflow that
fails partway is reported to the agent as a failed tool call, which is a different thing from one
that finished with nothing to return.

One trap: a handoff workflow that ends on a **Disconnect** node hangs the caller up. There is
nobody left to return to, so the agent never resumes. Leave the caller connected and let the
workflow run off the end of its path instead.

### Handoffs in website chat

<Callout type="info">
    Workflow handoffs in chat are being rolled out. Until they are on for your organization, an
    agent in a website chat isn't offered its **Workflow (Handoff)** tools. Contact support to turn
    them on.
</Callout>

In a [website chat](/docs/agents/chat-widget), a handoff moves the conversation to the workflow
without leaving the panel. The workflow's **Send Chat Message** nodes speak to the visitor, and the
visitor's replies go to the workflow until it finishes. The agent then replies straight away from
the workflow's result, as it does on a call. It sees the result, not the messages the workflow and
the visitor exchanged, so return what it needs from a **Response** node. A result longer than
4,000 characters is cut off at that length.

A workflow that reaches a **Disconnect** node while the visitor is in it ends the chat. If the
workflow reaches Disconnect after its reply timeout ran out, the chat stays open and the visitor's
next message goes back to the agent. Voice nodes such as Play Message and
the transfer nodes need a phone call and don't work in a chat; see [Answering with a
workflow](/docs/agents/chat-widget#answering-with-a-workflow). If the workflow can't start, or stops
to wait on something other than the visitor, such as a Time Delay, the agent takes the conversation
back and is told the workflow failed. The same happens when the visitor replies after the workflow
has stopped running, for example because its reply timeout ran out.

An agent can start at most two workflows from one visitor message. A third handoff in the same
message is refused. The agent is told so and answers the visitor in the same response, without handoff
tools. A handoff the agent gets wrong, such as a missing required argument, is not started either. The
agent is told that too, and answers in the same response.

The chat widget editor's preview doesn't offer handoff tools.

## Add an integration tool

The target dropdown lists the integrations your organization has connected, so set one up on
the Integrations page first — until you do, the dropdown has nothing to offer. What the row
asks for next depends on which one you pick.

### Zendesk

Connected Zendesk accounts appear in the dropdown under a **Zendesk** heading. Picking one
replaces the method and path fields with a single **Action**:

| Action            | What the agent does                                                                                                                         |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **Create Ticket** | Files a ticket. The model supplies the subject, the description, the requester's name and email, and optionally a priority, type, and tags. |
| **Show Ticket**   | Reads a ticket back by its number — its status, subject, description, type, priority, tags, and when it was opened and last updated.        |

You set the **Name** and the **Description**. There is no method, path, or parameter list,
because Tandem owns the request: the action decides what the model may send, so it can neither
reach an endpoint you didn't choose nor invent a field your Zendesk doesn't have.

**Create Ticket** adds four optional boxes for where the ticket lands. You fill them in once
and every ticket this tool files uses them; the model is never shown them, because an id from
your Zendesk is not something it can know.

- **Assignee Email** — the Zendesk agent to assign, by their sign-in email.
- **Assignee ID** — the same agent by numeric user id. Zendesk honours this over the email
  when you set both.
- **Group ID** — the group to queue the ticket to. Set this alone to hand off to a team
  rather than a person.
- **Brand ID** — the brand to file under, on a multi-brand account.

Leave all four blank and your Zendesk account's own triggers route the ticket, as they do
today.

A few things the model is deliberately _not_ allowed to fill in: custom fields, External ID,
and whether the first comment is visible to the requester. If you need those, use the **Create
Zendesk Ticket** node tool instead and fix them yourself — see
[Zendesk](/docs/workflows/integrations/zendesk) and [Add a node tool](#add-a-node-tool).

### Slack

Connected Slack apps appear in the dropdown under a **Slack** heading. Picking one replaces the
method and path fields with an **Action** and a destination:

| Action           | What the agent does                                                                  |
| ---------------- | ------------------------------------------------------------------------------------ |
| **Send Message** | Posts a message. The model writes the text; Slack formatting such as `*bold*` works. |

Under **Post To**, choose **Channel** or **Direct Message**, then pick the channel or the person.
If it isn't in the list, paste its Slack ID (`C0123ABCD` for a channel, `U0123ABCD` for a
person); sending needs only the `chat:write` scope, not the scopes that fill the list.
Every message this tool sends goes there. The model only writes the text and can't choose where
it goes, because it has no list of your workspace's channels to choose from.

- **Channel**: the Slack app must be a member. Run `/invite @your-app-name` in the channel first.
- **Direct Message**: the app needs the `users:read` scope to list people and `im:write` to
  message them.

If you pick a different Slack app on the row, the destination is cleared, because a channel in
one workspace doesn't exist in another.

The tool returns the message's `ts` and `channel` on success. A Slack rejection, such as the app
not being in the channel, comes back to the model as an error message it can relay. See
[Slack](/docs/workflows/integrations/slack#troubleshooting) for what each one means.

### Meridian

Connected Meridian integrations appear in the dropdown under a **Meridian** heading. Picking one
replaces the method and path fields with an **Action** and a **Require caller verification**
switch. The Meridian integration is being rolled out, so the heading appears only once it is on
for your organization.

| Action                            | What the agent does                                                                     |
| --------------------------------- | --------------------------------------------------------------------------------------- |
| **Find Account**                  | Searches for member accounts by phone, account, member, or meter number, or by address. |
| **Get Account**                   | Reads one account's balance and status.                                                 |
| **Get Outages**                   | Lists the active outages on an account.                                                 |
| **Report Outage**                 | Reports an outage on an account, with an optional note and callback.                    |
| **Check Arrangement Eligibility** | Checks whether an account can set up a payment arrangement.                             |

You set the **Name** and the **Description**. Tandem owns the request, so there is no method,
path, or parameter list.

With **Require caller verification** on (the default), every action except Find Account is
refused until the caller has been verified for that account in the conversation. Nothing records
a verification yet, so a tool with the switch on can only find accounts, and Find Account returns
account numbers only unless it searched by the number the caller is calling from. Turn the
switch off to let the tool read and report on any account number the model is given. See
[Meridian](/docs/workflows/integrations/meridian#ai-agent-tool) for what each action returns.

### Generic integrations

A generic integration tool works like a workflow tool, with one addition: you define the
request, so the model never chooses the HTTP method or the URL.

Pick the **Integration**, then set:

- **Method** — `GET`, `POST`, `PUT`, `PATCH`, or `DELETE`.
- **Request path** — relative to the integration's base URL, and it must start with `/`. A
  placeholder in braces is filled by the parameter of the same name whose **Location** is
  `path`:

    ```
    /customers/{customerId}/orders
    ```

Every parameter on an integration tool needs a **Location**, and it's explicit. Nothing is
inferred from the path or the method:

- `path` — substituted into the request path. The path placeholder and the parameter must have
  the same name, and the editor offers to create the missing parameter for you when you type
  the placeholder. Always required.
- `query` — appended to the query string.
- `body` — sent in the JSON request body. Not allowed on a `GET`; use `query` instead.
- `header` — sent as a request header. A header can't be filled by the model — use **Fixed** or
  **Call**. Header names are where an API key or token belongs.

To put a credential in a header or body value, reference a secret rather than typing it:
`Bearer {{secrets.stripe_key}}` in a **Fixed value**. The reference is resolved on the way
out, so the value never reaches the model or a log. Only an admin can save an agent that
references a secret, and secrets are allowed only in the request path and in a **Fixed** value
whose Location is `header` or `body`. See
[Secrets and variables](/docs/overview/secrets-and-variables).

Requests go only to public hosts. If the integration's base URL names a host that resolves to a
private or internal address, Tandem refuses the request before sending it. The tool call fails
with status 400, and the agent gets that error in place of a response.

## Shape what the agent gets back

By default the whole upstream response goes to the model. On a full customer record that's a
few thousand tokens the agent has to read before it can say one sentence, which the caller
hears as a pause.

Under **Response Fields**, click **Add Field** once per value the agent actually needs:

- **Name** — the key the agent receives it under, e.g. `balance`.
- **JSONPath** — where to find it in the response, e.g. `$.account.balance`. It's compiled when
  you save, so a malformed expression is an error you see now rather than a failure mid-call.
- **Default** — optional. Used when the expression matches nothing. Leave it empty and the key
  is left out; the agent then has nothing to read aloud, which is better than "your
  balance is null."

Leave the list empty and the agent gets the whole payload. With entries, it gets the status
plus only the fields you named. A non-2xx response skips extraction and returns the whole body,
since an error payload doesn't have the shape your expressions were written against.

## Wrap-up tools

A wrap-up tool is a generic integration tool that can still run after the call ends, when the
agent didn't get to it. The usual case is filing a support ticket for a caller who reported a
problem and then hung up.

Only a generic integration tool can be a wrap-up tool. A Zendesk, Slack, or Meridian action tool
can't, so to file a Zendesk ticket this way, point a generic integration at the Zendesk API.

To set one up, switch on **Wrap-up tool** on the tool's row, below **Response Fields**. A
**Wrap-up condition** box appears. Describe, in plain language, when the tool should run:

```
The caller reported a problem that was not resolved during the call.
```

The tool stays in the agent's toolset, and the agent sees it exactly as before. It can still call
the tool during the call, for example to read a ticket number back to the caller.

### What happens after the call

If the agent called the tool successfully during the call, or its call may have gone through
(see below), nothing more happens. Otherwise, shortly after the call ends, a model reads the
call's transcript and decides whether your condition is true. If it is, the model fills in the
tool's **Model** parameters from the transcript and Tandem sends the request. **Fixed** and
**Call** parameters are filled the same way they are during a call, so a phone number parameter
set to `call.ani` gets the caller's number.

A call in which the caller said nothing never runs a wrap-up tool after the call. When a caller
rings back after a dropped call and the agent resumes that conversation (see
[Reconnect after a dropped call](/docs/agents/voice-agents/brain#reconnect-after-a-dropped-call)),
the condition is judged only on what was said after the reconnect. The dropped call ran its own
wrap-up when it ended.

If the call moved to another agent, only the wrap-up tools of the agent the caller first reached
can run after the call. The agent the call moved to runs none of its own wrap-up tools after the
call, but if it has a wrap-up tool with the same name as one of the first agent's and calls it
successfully, the first agent's tool counts as **Sent during the call**. The summary one agent
passes to the next is shown to the model as a handoff note, so it isn't read as something the
caller said.

When the agent hands the call to a workflow with a **Workflow (Handoff)** tool, no wrap-up tool
runs at the handoff. When the caller comes back to the agent, the check after the call covers the
whole call. What the agent sent before the handoff still counts as **Sent during the call**, and a
tool is never sent twice across the two parts. If the caller hangs up while in the workflow, or
the workflow ends the call, no wrap-up tool runs, and the **Wrap-up** section says the call was
handed to a workflow.

**Test This Tool** sends the request the way a call would. It doesn't run the wrap-up check.

### At most once after the call

After the call, Tandem sends each wrap-up tool's request at most once. Whenever it can't tell
whether a request arrived, it doesn't send it again: a missing ticket can be filed by hand, and a
duplicate is harder to undo.

- If the agent's attempt during the call failed before it was sent, or the integration refused it
  (a `4xx` error other than `408`), Tandem still checks the condition after the call and can
  send it then.
- If a request timed out, was still waiting for an answer when the call ended, or the integration
  answered with a server error (`5xx`) or a redirect, Tandem can't tell whether it arrived. The
  tool is marked **Unknown** and isn't sent again.
- If the request sent after the call is refused, or can't be built from the transcript, it's
  marked **Failed** and isn't retried.

A successful request to a SOAP integration reads as **Unknown**, during the call and after it.
So a SOAP wrap-up tool that the agent called successfully during the call is never sent again.

One case can send a tool twice: a workflow with two **AI Agent** nodes whose agents both have a
wrap-up tool with the same name. The check after the first agent's part doesn't see what the
second agent does live, so the second agent can call the tool during the call and the first
agent's check can still send it. To avoid it, give the wrap-up tool to only one of the two agents.

When a tool shows **Unknown**, check the destination (your ticketing system, for example) before
you create the record by hand.

### Transcripts and your callers' data

A wrap-up tool reads the transcript, so an agent with one stores its call transcripts even when
**Store conversation transcripts** is off. The Safety card says so when that happens. See
[Transcript retention](/docs/agents/voice-agents/safety#transcript-retention).

The request is built from what the caller said, so it can carry a summary of their problem, their
name, their phone number, or account details. It goes to the integration you configured after a
call where the condition is true, unless the agent's own call to the tool during the call succeeded
or may have gone through. **Redact customer PII** doesn't change this: a stored transcript holds
the caller's words unmasked, and the parameters are filled from it.

### See what a wrap-up tool did

Open the call from **Insights → Call Logs** with the row's **View Transcript** button. Above the
transcript, a **Wrap-up** section lists each wrap-up tool the agent called during the call or
that the check after the call decided on, and what happened to it. A notice at the top of the
section says when that check is still running or didn't run at all; a tool the agent never
called has no row until the check decides on it.

| Shown                      | What it means                                                                                 |
| -------------------------- | --------------------------------------------------------------------------------------------- |
| **Sent**                   | Sent after the call. Shows what the integration returned, such as a ticket number.            |
| **Sent during the call**   | The agent called it successfully, so nothing ran after the call.                              |
| **Sending**                | The request is being sent now.                                                                |
| **Not needed**             | The condition wasn't met. Shows why.                                                          |
| **Failed during the call** | The agent's call failed and nothing ran after it. The notice above says why.                  |
| **Failed**                 | The request didn't go through. Shows the error.                                               |
| **Unknown**                | The request may have gone through. Check the destination before re-creating it.               |
| **Pending**                | The agent's call failed; the check after the call is still running.                           |
| **Unavailable**            | The check after the call hadn't finished an hour after the call ended. Check the destination. |

What **Sent** shows follows your **Response Fields**: the fields you named, or the whole response
if you named none. A response over 8 KB, or one that can't be stored, is kept as its status code
only and shows as, for example, `200 (response not stored)`.

## Test a tool before you call

Generic integration tools have a **Test This Tool** section, so you don't need a phone call to
find out whether the request is right. A Zendesk or Slack tool has none — there is no request of
yours to preview; use **Test Connection** on the integration itself instead. Fill in a sample
value for each **Model** parameter — `call.ani` and `call.id` too, if you use them; `agent.id`
and `org.id` are sent for you — then:

- **Preview Request** resolves the request and shows it without sending anything.
- **Run Live** actually calls your API. On a method that changes data it asks for confirmation
  first, naming the request it's about to send.

The section also works on a published version, so you can run both buttons against the tools
callers reach.

A live run shows the **Resolved Request**, the **Raw Response** with the status your API
returned, **Sent to the Model**, and the **Response Headers**. The raw response and what the
model receives each show a byte count, so you can see how much your Response Fields cut. Each
payload has a copy button and an expand button that opens it in a larger window.

## Nested request bodies

A parameter's type can be **Object** instead of a scalar. That turns it into a _container_: a
group that holds its own nested parameters instead of a value of its own. Nesting is available
only for a **Body** parameter on an **integration** tool — a workflow or handoff tool's
parameters become variables, which have no place to put a nested value, so `object` isn't
offered there.

A container has no Model, Fixed, or Call choice, because it has nothing to fill — only its
children do, and each child chooses independently. A child can itself be another object, down
to the depth limit below. A nested child never sets its own location: it always goes wherever
its top-level ancestor does.

A **Fixed** leaf never reaches the model, at any depth. That's true at the top level today, and
nesting doesn't change it — an author-set value inside an object is exactly as invisible to the
model as one outside it.

**Limits:**

- A parameter tree may nest at most **3 levels deep**, counting the top level as level 1.
- A tool may declare at most **50 parameters in total**, containers included — so a container is
  not free even though it has no value of its own.

### Worked example

Say the integration expects this body:

```json
{ "type": "select", "condition": { "objectId": "1001", "objectType": "utilityProvider" } }
```

You'd author it as three parameters, one of them nested:

- **`type`** — string, Body, **Fixed**: `"select"`
- **`condition`** — object, Body, described for the model as what it should fill in. Holds:
    - **`objectId`** — string, **Model**, required, described as the item's ID
    - **`objectType`** — string, **Fixed**: `"utilityProvider"`

At call time, the tool builds exactly the body above — `type` and `objectType` come from what
you set, and `objectId` from whatever the model supplied (here, `"1001"`).

But the model never sees `type` or `objectType`; both are **Fixed**, so they're pruned from the
schema it's shown. What it sees is only:

```json
{
    "type": "object",
    "properties": {
        "condition": {
            "type": "object",
            "description": "The item to select.",
            "properties": {
                "objectId": {
                    "type": "string",
                    "description": "The item's ID."
                }
            },
            "required": ["objectId"]
        }
    }
}
```

`condition` survives because it has at least one Model child (`objectId`); a container whose
every child is Fixed or Call is pruned from the schema entirely, at any depth — the model is
never shown an empty object to fill in.

## Add an MCP tool

Choose **MCP** as the kind and pick the MCP integration. That's the whole row — no name,
description, or parameters, because the MCP server publishes those itself and the agent reads
them when the call starts.

Which tools the agent can reach is controlled on the integration, by the tools you've enabled
there. Because you don't write those descriptions, test an MCP-backed agent on real calls
before trusting it: the agent's judgment about when to use each tool is only as good as the
descriptions the server ships.

The integration's **Server URL** must start with `https://`, and Tandem does not follow
redirects from it. A server that answers with a redirect of any kind, including one that only
adds a trailing slash, fails in two places. Loading its tools on the integration fails with an
error saying the server redirected. And when a call starts or a chat message arrives, the agent
fails to connect to it and answers without that server's tools; the caller or visitor sees no
error. Save the URL the server answers on directly. If an MCP-backed agent never uses its tools,
check the integration's **Server URL** for a redirect.

Loading a server's tools on the integration also fails when the **Server URL**'s host resolves
to a private or internal address.

## Add an agent tool

Choose **Agent** as the kind and pick another agent in your org from the second dropdown, then
name and describe the tool the same way you would a workflow tool — the **Description** is what
tells the caller-facing agent when to reach for this specialist.

Unlike the other kinds, an agent tool has no **Parameters** to declare. The caller-facing agent
always hands the linked agent a single message describing what it needs; the linked agent
decides what to do with it and hands back a plain-text answer, which the caller-facing agent
then speaks to the caller. The linked agent runs its own persona, model, and tools for that one
exchange. It never talks to the caller directly, and it starts fresh on every call to it, so
the caller-facing agent's message has to carry whatever context the linked agent needs.

**A linked agent can't itself link another agent.** You can build one level of delegation — a
caller-facing agent consulting specialists — but a specialist's own tool list may hold any kind
except another agent tool. This keeps a call from chaining through an unbounded sequence of
agents. It also can't be linked to itself.

## Add a node tool

A node tool runs one workflow node directly, in place, when the model calls it — the same node
type you'd drop on a workflow canvas, minus the canvas. The caller stays with the agent
throughout, exactly as with a Workflow (Invoke) tool; the difference is scope, not call
behavior. Reach for a workflow tool when the action needs several steps or branching logic, and
for a node tool when it's genuinely one step.

<Steps>
<Step>
### Add the row and choose Node

Click **Add Tool**, then set the kind dropdown to **Node**.

</Step>

<Step>
### Pick the node

The **Node** dropdown lists every node type, grouped the same way the workflow canvas groups
them. Most are shown disabled with the reason underneath their name — see
[Why a node is unavailable](#why-a-node-is-unavailable).

</Step>

<Step>
### Name and describe it

Same rules as a workflow tool: a snake_case verb phrase the model calls, and a description that
says when to use it. See [Add a workflow tool](#add-a-workflow-tool) above.

</Step>

<Step>
### Fill in the node's own fields

Below the picker, the node's own field editor appears — the identical form you'd see configuring
that node on a workflow canvas. Every field starts **author-fixed**: you set it once, and it's
sent the same way on every call.

A field whose type is a plain scalar — text, a text area, a dropdown, a switch, a number, a
phone number, or an email address — carries a **Model-filled** switch beneath it. Turn it on and
the field moves out of the node's own editor and into the **Parameters** list below, where it
works exactly like a workflow tool's Model/Fixed/Call parameter (see [Parameters](#parameters)
above) — except it fills that node field instead of a workflow's starting variable.

A field whose type isn't on that list — a dynamic dropdown populated from your data, an array of
objects, rich text, a date picker — has no switch at all. It stays author-fixed, always. Those
are precisely the field types a model must not be trusted to fill freely, so the option to hand
one to the model is never offered.

</Step>

<Step>
### Save

Click **Save Configuration**. The tool is available on the agent's next call. On an agent that is
on [versioning](/docs/agents/voice-agents/versioning), you'll be editing a draft — the button reads
**Save as New Draft** if you started from the published version — and the tool reaches callers when
you publish that draft.

</Step>
</Steps>

### Reading the result

A node tool answers with `{outcome, data}`. **`outcome`** is the exit name the node itself took —
the same name you'd see as an edge label on the workflow canvas. It is carried over verbatim, not
translated into a generic success/failure flag. **`data`** is the node's response payload,
trimmed by [Response Fields](#shape-what-the-agent-gets-back) exactly as an integration tool's
is, if you've added any.

This matters for how you write the description and the system prompt: a node's built-in exits
like `fallback`, `invalidInput`, or `timeout` come back as an ordinary `outcome` value, not as a
tool error. The model is meant to read them and recover in the conversation ("the email address
didn't work, want to double check it?"), the same way it would react to any other spoken answer.
One outcome has no matching edge on the canvas: a node that stops the run outright rather than
leaving by a named exit answers with `outcome: "terminal"`. Treat it as a failure the agent
should acknowledge rather than retry.

A tool call only reports `{error}` for what the model genuinely cannot act on: the node
throwing, the call exceeding the tool's timeout, or a node type this environment can't run at
all.

### Why a node is unavailable

Whether a node can be bound is not a hand-picked list. It is derived from what this environment
can actually do, checked in this order:

- **The node is deprecated.** It's being phased out and was never opened to new tool bindings.
- **The node can pause the call.** A node that waits for something outside itself — a reply, a
  scheduled follow-up — can't complete as a single synchronous tool call. A small number of
  these become bindable once you fix the field that would trigger the wait to its non-waiting
  value; the picker only lists them as available once you've done that.
- **The node has no implementation in this environment.** Most of the workflow node catalog
  isn't reachable from an agent at all yet.
- **The node needs something this environment doesn't have.** A node that reads or writes data
  this call-handling environment has no access to is correctly unavailable rather than silently
  failing partway through.

An unavailable node is always shown, disabled, with the specific reason underneath its name —
never hidden. If a node you need isn't offered, the reason tells you which of the above applies;
it isn't a bug to work around.

**Available today:** Log Event, Send Email, and Send SMS. More node types open up as this
service grows what it can run — check the picker for the current list rather than treating this
as fixed.

### Comma-separated lists

A few node fields — a Send Email recipient list, for example — are a list of values on the
workflow canvas. A model can't be handed a real list as a parameter type, so when you make one
of these fields model-filled, the model instead fills in a single comma-separated string, which
is split back into the list the node expects before it runs. This is safe for addresses
specifically because a valid email address can never contain a comma, so splitting on `,` never
cuts one in half. Treat it as a convention for this field type today, not a general way to hand a
model a list — true list parameters may arrive later.

## Writing tools an agent uses well

Tandem enforces none of this.

- **One job per tool.** A `manage_account` tool that reads, updates, and cancels will be
  called for the wrong one. Split it into three tools with three descriptions.
- **Say when to use it, not just what it is.** "Use this when the caller asks about delivery
  timing" gives the model a trigger. "Order lookup endpoint" doesn't.
- **Describe every Model parameter.** An undescribed parameter gets filled with a plausible
  guess — and a save without one is rejected.
- **Don't ask the model for what the call already knows.** The caller's number is
  `call.ani` as a **Call** parameter. Asking the model for it invites a wrong answer and lets a
  caller name someone else's.
- **Mark the required ones required.** Otherwise the agent will happily call the tool without
  them.
- **Say what the tool returns**, in the description, if the shape isn't obvious. The agent has
  to turn that result into a spoken sentence.
- **Trim the response.** Use **Response Fields** to send the five values that matter instead of
  the whole record. It produces better answers and faster ones.
- **Name the tool for the caller's intent**, not your system's internals. `check_balance` beats
  `acct_svc_v2_query`.
- **Set a [thinking cue](/docs/agents/voice-agents/voice-and-speech)** once an agent has any
  tools. Every tool call is a pause, and an uncovered pause sounds like a dropped call.
- **Say what to do on failure** in the system prompt: "If the lookup fails, apologize and
  offer to take a message." Otherwise the agent improvises.

## Limits and defaults

| Setting                      | Value                                                                                |
| ---------------------------- | ------------------------------------------------------------------------------------ |
| Tools per agent              | No limit                                                                             |
| Tool name                    | Letters, numbers, hyphens, underscores; up to 64 chars                               |
| Tool name                    | Must be unique within the agent                                                      |
| Tool name                    | Can't be one of the platform's own (see below)                                       |
| Description                  | Required on every kind except MCP                                                    |
| Parameters                   | Up to 50 per tool, containers included; name up to 64 chars                          |
| Parameter nesting            | At most 3 levels deep; **Object** only on an integration body                        |
| Parameter **Type**           | `string`, `number`, `integer`, `boolean`, `object`                                   |
| Parameter **Location**       | `path`, `query`, `body`, `header` — required on integration tools                    |
| Response Fields              | Up to 50 per tool; name up to 64 chars                                               |
| Integration **Method**       | `GET`, `POST`, `PUT`, `PATCH`, `DELETE` — generic integrations only                  |
| Integration **Request path** | Required; must start with `/` — generic integrations only                            |
| Wrap-up tools                | Up to 5 per agent; generic integration tools only                                    |
| Wrap-up condition            | Required when **Wrap-up tool** is on; up to 2,000 characters                         |
| Zendesk tool                 | No parameters and no Response Fields; **Action** is `Create Ticket` or `Show Ticket` |
| Slack tool                   | No parameters and no Response Fields; a channel or person is required                |
| MCP tool                     | No name, description, or parameters — discovered at runtime                          |
| Agent tool                   | No parameters; cannot target itself or a chain of agents                             |
| Node tool                    | `nodeType`/version pinned at save time; only bindable nodes offered                  |

These names are taken by the platform's own tools and can't be reused: `end_call`,
`search_knowledge_base`, `skills`, `transfer_to_external`, `transfer_to_voice_agent`,
`capture_dtmf`, `pause_recording`, `resume_recording`, `get_current_time`, and
`set_workflow_variable`. See [Built-in Tools](/docs/agents/voice-agents/built-in-tools).

Changing a tool's kind keeps the name, description, parameters, and response fields, and clears
only the target you'd picked, so correcting a mis-set kind doesn't cost you the rest of the
row.

## Troubleshooting

**The agent never uses a tool.** Nearly always the description. Rewrite it to name the
situation ("Use this when the caller asks…") rather than the endpoint. Also confirm the tool
saved — check the row is still there after a reload.

**The agent uses the wrong tool.** Two descriptions overlap. Sharpen the boundary in both, and
consider whether they should be one tool.

**The agent calls a tool with empty or invented arguments.** Every **Model** parameter needs a
description, and the ones the tool can't work without need **Required** on. Open **What the
model sees** to confirm the schema is what you think it is.

**A value never reaches the API.** Only **Model** parameters appear in **What the model sees**;
a **Fixed** or **Call** parameter is added on the way out. Use **Preview Request** to see the
fully resolved request.

**"Tool … path references `{customerId}` but no path parameter declares it."** Path
placeholders aren't inferred. Add a parameter named `customerId` with **Location** `path` — the
editor offers an **Add `customerId`** button that does exactly that.

**"… header … cannot be filled by the model; use a fixed or context value."** Headers are
author-supplied by design. Switch that parameter's **Value from** to **Fixed** or **Call**.

**"… cannot be in the body on a GET; use a query parameter."** Change the **Location** to
`query`, or change the **Method**.

**"… must be filled by the model; fixed and call values are not passed to the workflow."** A
**Workflow (Handoff)** tool takes **Model** parameters only. Switch to **Workflow (Invoke)** if
the tool needs fixed or call values, or drop those parameters.

**"Duplicate tool name" or "reserved tool name."** Names must be unique within the agent, and
the ten platform names listed above are taken.

**"Tool … needs a wrap-up condition."** Fill in **Wrap-up condition**, or switch **Wrap-up
tool** off.

**"Tool … wrap-up condition is limited to 2000 characters."** Shorten the condition. The limit
applies with the switch off too, because a switched-off tool keeps its condition.

**"An agent can have at most 5 wrap-up tools."** Switch **Wrap-up tool** off on the tools that
don't need to run after the call.

**A wrap-up tool didn't run after a call.** Open the call in Call Logs and read its **Wrap-up**
section. **Sent during the call** and **Not needed** are the usual reasons. **Unknown** means it
may already have gone through, so it isn't sent again, and **Failed during the call** means the
agent's own attempt failed and the check after the call didn't run; the notice above the rows says
why. A call in which the caller said nothing never runs one after the call.

**Saving fails on a secret reference.** Only an admin can save an agent that references
`{{secrets.*}}`, and a reference is only accepted in an integration tool's request path or
in a **Fixed value** whose Location is `header` or `body`.

**The agent goes quiet during a tool call.** Set a **Thinking cue** on
[Voice & Speech](/docs/agents/voice-agents/voice-and-speech).

**The agent won't take an action even though the tool exists.** Check
[Safety](/docs/agents/voice-agents/safety) — a steering rule may be blocking that call.

**"An agent tool cannot target the agent it is configured on" / "already links another agent as
a tool."** Self-reference and chaining through a second agent tool aren't allowed — see the
depth limit above. Point the tool at a specialist that has none of its own agent tools.

**A node is grayed out in the picker.** That's not a bug — it's shown disabled with the reason
underneath its name. See [Why a node is unavailable](#why-a-node-is-unavailable).

**A node tool's result reads like an error but the call kept going.** That's expected for a
system exit (`fallback`, `invalidInput`, `timeout`) — it arrives as an ordinary `outcome` value,
not a tool failure, so the model can recover conversationally. See
[Reading the result](#reading-the-result).

## Next steps

<Cards>
    <Card
        title="Built-in Tools"
        href="/docs/agents/voice-agents/built-in-tools"
        description="Transfers, DTMF capture, and the other call-control tools you switch on."
    />
    <Card
        title="Skills"
        href="/docs/agents/voice-agents/skills"
        description="Named procedures the agent follows when the situation calls for them."
    />
    <Card
        title="Safety"
        href="/docs/agents/voice-agents/safety"
        description="Constrain which tool calls the agent may make."
    />
    <Card
        title="Using an Agent in a Workflow"
        href="/docs/agents/voice-agents/using-in-workflows"
        description="Hand a call to an agent and route on the outcome."
    />
</Cards>
