# Building with AI (/docs/overview/building-with-ai)

> 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.

AI appears in four places on Tandem. They are separate features with separate configuration,
and picking the wrong one is the usual reason something is harder to build than it should be.
If you want to go the other way and give the AI assistant you already work with access to
Tandem, skip to [Connect your own AI assistant](#connect-your-own-ai-assistant).

## Voice agents

A [voice agent](/docs/agents/voice-agents) answers a phone call, listens, decides what to
say, calls out to your systems when it needs to, and reports how the call ended. You give
it a system prompt, a voice, a model, and a set of tools; it works out the rest at call
time.

The same agent answers on your website through the [chat
widget](/docs/agents/chat-widget), so an agent you have already tested by phone can go on
a web page without being rebuilt.

Use an agent when the conversation is open-ended and you cannot enumerate what the caller
will say.

## Workflows

A [workflow](/docs/workflows) is a graph you draw: check the time, look a record up,
branch, send a message. It does exactly what you wired and nothing else.

Use a workflow when the path is known: a menu, a business-hours check, a notification, a
lookup with two outcomes. A workflow costs no model call and adds no model latency, and it
takes the same path every time, so what you tested is what runs.

The two compose. A workflow hands a call to an agent with the **AI Voice Agent** node and
routes on the outcome; an agent runs a workflow as a
[tool](/docs/agents/voice-agents/tools) when it needs a defined sequence of steps. See
[Using an Agent in a Workflow](/docs/agents/voice-agents/using-in-workflows).

## Knowledge bases

Anything the agent should quote (hours, fees, policies, product detail) belongs in a
[knowledge base](/docs/agents/knowledge-base/overview) rather than in the prompt. The agent
searches it during a call and answers from what it finds.

The practical reason is maintenance: a fact in a knowledge base can be corrected without
re-testing the prompt, and one knowledge base serves every agent that attaches it.

## Tandy

Tandy is the assistant inside the product. Your callers never reach it. It can build a
voice agent or a workflow, [change an agent you already
have](/docs/agents/voice-agents/editing-with-tandy), schedule a workflow, reroute a phone
number, invite or remove a user from [Manage
Users](/docs/overview/manage-users#manage-users-with-tandy), run a [security
review](/docs/agents/voice-agents/security-review) over one of your agents and offer to
apply the fixes, and [report a problem or request a
feature](/docs/overview/report-a-problem) on your behalf.

Tandy can also make any other change the [MCP server](#the-tandem-mcp-server) offers, such
as editing a workflow's steps or publishing a version.

Tandy knows which page you sent each message from. In the workflow editor, the voice agent
editor, or a conversation in **Insights → Call Logs**, you can say "this workflow", "this
agent", or "this call" and Tandy reads that one. It reads the last saved copy, so save your
edits before you ask about them.

Tandy never makes a change on its own. It shows you a card describing what it understood,
and nothing happens until you press the button on it. For the changes above, the card is a
draft you can adjust. For any other change, the card is an approval: it names the action and
every value Tandy will send, you cannot edit it, and **Approve** makes exactly that call. To
change a value, decline the card and tell Tandy what to change.

Tandy keeps your chats on Tandem's servers, per user and per organization. They appear on every
device you sign in on, and nobody else in your organization can see them. A chat is deleted 90
days after its last message. An attached file is kept for 30 days, then its chip shows
**Expired** and Tandy is told the file is gone. **Delete** on a chat and **Delete All** remove
chats immediately. When an administrator removes or disables you in [Manage
Users](/docs/overview/manage-users), your chats in that organization are deleted, along with
your chats in any [sub-tenant](/docs/overview/sub-tenants) the disable removed you from. If you
are removed some other way, they expire after 90 days. Personal access tokens cannot read Tandy
chats.

## After the conversation

Two features read finished conversations rather than running them:

- [Post-call analysis](/docs/agents/voice-agents/post-call-analysis) reads a call's
  transcript once it ends and pulls out sentiment, an outcome, a summary, and any fields you
  define.
- [Insights](/docs/workflows/insights-and-reporting) reports volume, call logs, and the
  paths conversations actually took.

## Connect your own AI assistant

Everything above is AI running inside Tandem. This section is the other direction: giving the
assistant you already write code with access to the Tandem documentation and to your own
organization's configuration, so it can answer questions about Tandem without you pasting
pages into it.

There are two ways in. The MCP server lets an assistant call Tandem and read your own data;
the `llms.txt` files are static and need no setup at all.

### The Tandem MCP server

Tandem hosts a [Model Context Protocol](https://modelcontextprotocol.io) server at
`https://mcp.tandemcx.ai`, also reachable at `https://mcp.spark.sharpencx.com`. Both
addresses reach the same server. Any MCP client can connect to it with a personal
access token. The server then acts as you, in the organization the token belongs to, with
exactly the permissions your own account has.

Fifty-nine tools are available:

- **Documentation** — `search_docs` and `fetch_doc` search and read these pages. They are the
  only two that work without a token.
- **Your organization** — `list_phone_numbers`, `list_sip_trunks`, `list_workflows`, `get_workflow`,
  `list_knowledge_bases`, `list_integrations`, `list_voice_agents`, `get_voice_agent`,
  `list_voice_agent_versions`, `list_agent_models`, `list_voice_options`, `get_me`,
  `list_users`, `list_roles`, `list_chat_widgets`, `get_chat_widget`, `list_chat_updates`,
  and `get_chat_update`.
- **Conversations** — `list_conversations`, `get_conversation`, and
  `get_conversation_transcript` read what **Insights → Call Logs** shows: which calls and chats
  happened, what each one did, and what was said.
- **Workflow authoring** — `list_node_types`, `get_node_schema`, `list_node_field_options`,
  `find_nodes`, and `build_workflow_draft`, which turns described steps into a node graph Tandem
  will accept. `build_workflow_draft` creates nothing. It returns either the graph or every reason
  the steps cannot become one.
- **Agent tests** — `list_agent_tests`, `get_agent_test`, and `get_agent_test_run` read an
  agent's [tests](/docs/agents/voice-agents/tests) and their results.
  `draft_agent_test_from_conversation` drafts a test from a past conversation and saves nothing.
- **Twenty-seven writes** — `create_voice_agent`, `update_voice_agent`,
  `publish_voice_agent_version`, `create_workflow`, `update_workflow`, `update_workflow_nodes`,
  `create_workflow_version`, `publish_workflow_version`, `schedule_workflow`,
  `update_phone_number`, `update_sip_trunk`, `invite_user`, `delete_user`, `create_agent_test`, `update_agent_test`,
  `delete_agent_test`, `run_agent_tests`, `create_chat_widget`, `update_chat_widget`,
  `delete_chat_widget`, `create_chat_update`, `update_chat_update`, `publish_chat_update`,
  `unpublish_chat_update`, `delete_chat_update`, `create_integration`, and
  `update_integration`. Every one is marked as destructive in the tool definition,
  so a client that reads that marking stops and asks you to approve the call before it runs.
  `run_agent_tests` spends model tokens.

`create_integration` and `update_integration` take no credential. An integration is created
with no authentication, and you add its password, API key, or AWS keys on its settings page
under **Integrations**. For the same reason, `update_integration` can't change a REST/SOAP
integration's base URL, or an MCP server integration's URL while it has a stored API key; change
those on the settings page too. A new MCP server integration has every tool disabled. When the server
can be reached, the create call returns the tools it offers, and `update_integration` with
`enabledTools` turns them on.

A write that takes a payload, such as an agent configuration, a workflow, or a test, reports a
rejection per field, so a client can correct one value and retry rather than re-sending the
whole request blind.

`create_voice_agent` takes an agent configuration in the same shape `get_voice_agent`
returns and saves it as version `0.1`. **A new agent is not live** until a version is
published: call `publish_voice_agent_version` with that version, or publish it in the agent
editor. Build the configuration from the
catalog tools rather than from guesses: `list_agent_models` for the model, `list_voice_options`
for the voice, `list_workflows` and `list_integrations` for tools, `list_knowledge_bases` for
knowledge. If Tandem rejects the configuration, the error names each field and why, so the
call can be corrected and retried.

`update_voice_agent` changes settings on an agent that already exists. It takes a list of
`{path, value}` changes — `safety.redactPii`, `limits.maxCallDurationMs`, `systemPrompt` —
rather than a whole configuration, because it reads the agent's current settings and applies
your changes to those. That is what stops the settings you did not mention from being wiped,
and it is the only way to change one field of an agent whose variable values you cannot read.
Pass `observed_updated_at` (the `updatedAt` from your `get_voice_agent` read) and the call
refuses rather than overwriting a change someone else saved while you were working.

On an agent that has a published version, `update_voice_agent` saves your changes as a new
version and publishes it, so they are live when the call returns. On an agent with nothing
published it refuses with `DRAFT_NOT_EDITABLE`; publish a version with
`publish_voice_agent_version` first. The same tool rolls an agent back: pass an earlier
version from `list_voice_agent_versions`. Publishing checks the whole configuration, and a
refusal names each field and why.

`create_chat_widget` and `update_chat_widget` manage a [chat widget](/docs/agents/chat-widget).
A widget answers with exactly one agent or one workflow: the agent needs **Chat** among its
[Channels](/docs/agents/voice-agents/persona#channels), and the workflow needs a published
version. An empty `allowedOrigins` list
means the widget works on no site. `update_chat_widget` takes only the fields that change and
keeps the rest; `null` clears a field, and a list such as `allowedOrigins` is replaced whole.
Sending a new `agentId` or `workflowId` clears the other. `delete_chat_widget` stops the embed
snippet on your site working at once.

`create_chat_update` saves a post for the [Updates tab](/docs/agents/chat-widget/updates) as a
draft. Visitors see it only after `publish_chat_update`, and `unpublish_chat_update` takes it
back to a draft. The post body is a list of blocks (headings, paragraphs, quotes, lists, images,
and one button), not HTML or Markdown; `create_chat_update` lists the exact shapes. An empty
`widgetIds` list shows the post on every widget with the Updates tab turned on.

Both update tools take `observed_updated_at` (the `updatedAt` from `get_chat_widget` or
`get_chat_update`) and refuse with `STALE_CONFIG` rather than overwriting a change someone saved
while you were working. A field Tandem does not recognize, or one it sets itself such as a
post's `state`, is not saved, and the answer lists it under `notApplied`.

`list_node_field_options` lists the real values for a step setting backed by a dropdown: a
prompt file, a queue, a voice. Some of those settings depend on another setting beside them.
`get_node_schema` reports that as `derivedBy`, naming the setting whose value the lookup
needs, and a few name two. Play Message's **Voice Engine** is the one to know about. A voice
name is only unique within a language, so the engine list needs the language and the voice
together: pass both as a list, in the order the schema gives them. One value on its own comes
back empty, which reads as though your organization has no engines to choose from.

`create_workflow` commits a graph from `build_workflow_draft`. **A new workflow is not
live**: it has no published version until someone publishes it, with
`publish_workflow_version` or in the workflow editor, and an
unpublished workflow does not answer calls or run on a schedule. `schedule_workflow` refuses
an unpublished workflow for that reason, rather than arming a series that would fail every
occurrence.

`get_workflow` returns a workflow's settings and the steps of one version. The steps are a
separate record from the workflow itself, so the call reads both. It reports `published` as
its own field: an unpublished workflow does not answer calls or run on a schedule, however
complete its steps look. Omit `version` for the published one, or for the newest draft when
nothing is published.

The steps come back in full by default, which on a large workflow is a large answer. Two
options narrow it: `include="outline"` returns each step's name, type, and exits but not its
settings, which is what reading the shape of a flow needs; `names` returns only the steps you
name, in full. Find the step in the outline, then read that one. On a workflow of fewer than
about ten steps the full answer is already small enough that one call beats two.

`update_workflow` changes a workflow's name, description, or timezone, its steps, or both.
Settings merge, so a rename leaves the description and timezone alone. Steps go to one
version, and **a published version cannot be updated** — that is what live calls are running,
so change a published workflow through a new version (see below). Settings and steps
are separate records and are written in separate calls, so the answer lists what actually
landed rather than implying both did. Pass `observed_updated_at` — the `workflow.updatedAt`
from your `get_workflow` read — and the call refuses with `STALE_WORKFLOW` rather than
overwriting a change someone saved in the editor while you were working.

`find_nodes` answers questions about a workflow's steps without reading it whole: which step
plays a given prompt file, which steps transfer to a queue, what leads to a given step. The
outline above shows names, types and wiring; this searches the settings, including the nested
ones on a Switch evaluation or a Time Control event, and tells you which setting matched. Give
at least one of `text`, `node_type`, or `exits_to`; they combine with AND. Long values are
shortened, so a match tells you where to look rather than handing back the whole setting.

`update_workflow_nodes` changes individual steps. Prefer it over `update_workflow` for editing
a workflow that already exists: `update_workflow` replaces every step, so your tool has to read
the whole workflow and send it all back to change one setting, while this one merges on the
server and you send only what changes. You give it a list of operations, applied in order:
`setFields` and `setExits` on a named step, `add`, `remove`, and `rename`. Steps are identified
by name, so `rename` also repoints every exit aimed at the old name and tells you which steps it
touched. `setFields` merges, so changing a prompt leaves the language and voice beside it alone.
The whole workflow is checked before anything is written: if removing a step leaves another one
pointing at it, nothing is saved and you get the reason per step. A published version cannot be
changed, the same as `update_workflow`.

To change a published workflow, make a draft, edit it, and publish it:

1. `create_workflow_version` copies the published version (or the one you name in
   `from_version`) into a new unpublished version. It is numbered one above the highest version
   the workflow holds, so `1.3` becomes `1.4`, and the answer tells you the number.
2. `update_workflow_nodes` or `update_workflow` edits that draft. Pass the new number as
   `version`. Callers keep getting the published version while you work.
3. `publish_workflow_version` makes the draft the version live calls and schedules run.
   Tandem checks the whole workflow first and refuses a version with errors, naming each
   step and what is wrong with it.

Each new version counts toward your organization's version limit, so make one draft per round
of edits rather than one per change.

`list_conversations` returns the Call Logs list, newest first, with the same filters: a start
time range, workflows, statuses, channels, and a search over conversation ids, workflow names,
and phone numbers. With no range it covers the last 30 days, and with only an end time it
covers the 30 days before it. A range longer than 90 days is rejected. `get_conversation`
returns one conversation's record, the log of what happened on it (the agent answering, a
workflow starting, a transfer), and each workflow run with the steps it took and the exit each
step chose. Conversations from before that log was recorded report
`actionsAvailable: false`. Their workflow runs are still returned.

`get_conversation_transcript` takes the same conversation id and works for both calls and
chats. Tool calls the agent made appear as turns, with their arguments and, when a call failed,
the reason. A transcript that was never stored or has expired comes back with
`available: false` and a reason rather than an error. Voice transcripts are kept for 90 days.
The transcript is the caller's own words and can include personal details, and your AI tool
receives it in full.

`update_phone_number` changes where a number routes or what it is called, and takes effect on
the next inbound call.

A SIP trunk number is not a phone number to these tools. `list_phone_numbers` leaves it out;
`list_sip_trunks` lists it under its trunk, and `update_sip_trunk` changes where it routes. A
trunk has a default route that every number without its own route uses, so for a trunk with one
number, route the trunk. To route one number differently, send the trunk's whole `numbers` list
with that one entry changed: the list replaces the stored one, so a number left out is removed.
That change also needs the trunk's `updatedAt` from `list_sip_trunks`, and it is refused if the
trunk changed after that read. `update_sip_trunk` changes routing only; a trunk's connection
settings, such as its credentials and allowed IPs, are changed in Tandem itself. A trunk managed by the Sharpen integration refuses a change to its numbers, though its default
route can still change.

`get_voice_agent` returns an agent's stored configuration, and it never returns the value of
an agent variable. You get the name, the length, and whether the value looks like a
credential, which is enough to review an agent for a pasted secret without copying that
secret into the conversation. Tandy's [security
review](/docs/agents/voice-agents/security-review) works from the same reads.

It reads an agent you have not published yet. By default it returns the configuration the
agent answers calls with; if the agent is serving nothing — which every agent is between
being created and being published for the first time — it returns the newest version saved
instead and sets `published` to `false`. Read that field before you act on the answer: a
draft is not what callers hear, however finished it looks. `list_voice_agent_versions` lists
every version an agent holds and marks the one that is live, and passing a `version` to
`get_voice_agent` reads that one specifically.

#### Before you start

<Callout type="info">
    **Access tokens are being rolled out.** If **Access Tokens** isn't in your user menu, it isn't
    enabled for your organization yet — contact support.
</Callout>

You need a personal access token. Open the user menu (your avatar, top right), choose
**Access Tokens**, and select **Create Token**. The token is shown once, so copy it before
you leave that screen. [Authentication](/docs/api/introduction/authentication) covers
expiration, revocation, and the per-organization limit.

#### Set it up

<Tabs items={['Claude Code', 'Other MCP clients']}>
    <Tab value="Claude Code">
        ```bash
        claude mcp add --transport http tandem \
          https://mcp.tandemcx.ai \
          --header "Authorization: Bearer $TANDEM_PAT"
        ```

        Add `--scope user` to make the connection available in every project rather than only
        the current one. Run `/mcp` inside Claude Code to see the server's status, or
        `claude mcp list` from a shell.
    </Tab>

    <Tab value="Other MCP clients">
        Most clients are configured from a JSON file of roughly this shape. Check your client's
        own documentation for which file it reads and what it names the keys.

        ```json
        {
            "mcpServers": {
                "tandem": {
                    "type": "http",
                    "url": "https://mcp.tandemcx.ai",
                    "headers": {
                        "Authorization": "Bearer spark_pat_..."
                    }
                }
            }
        }
        ```

        The transport is streamable HTTP. There is no SSE endpoint, and a browser-based client
        cannot connect: the route answers no CORS preflight.
    </Tab>

</Tabs>

#### Check that it worked

This asks the server for its tool list, which needs no Tandem data and so fails only if the
connection itself is wrong:

```bash
curl -sX POST https://mcp.tandemcx.ai \
  -H "Authorization: Bearer $TANDEM_PAT" \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

A JSON-RPC response listing the tools means you are connected. Two failures are worth telling
apart:

- **`401 Unauthorized`** — the token is wrong, expired, or revoked. Create a new one.
- **`404 Not Found`** — usually the old endpoint. The server used to answer at
  `https://api.spark.sharpencx.com/v1/mcp`, which has been retired; the address is now
  `https://mcp.tandemcx.ai`, with no path after the host.

A revoked token can keep working for up to a minute, because the gateway caches its
verification decision.

### llms.txt and Markdown pages

Every page in this portal is also published as plain Markdown, so an assistant can read the
documentation without an MCP connection.

| File                                     | What it holds                                                       |
| ---------------------------------------- | ------------------------------------------------------------------- |
| `https://docs.tandemcx.ai/llms.txt`      | An index of every page: title, URL, and one-line description.       |
| `https://docs.tandemcx.ai/llms-full.txt` | The full text of every page, concatenated.                          |
| `https://docs.tandemcx.ai/sitemap.xml`   | Every page's canonical URL, for a crawler that wants the page list. |
| `<any docs URL>.md`                      | That one page as Markdown, headed with its title and canonical URL. |

The last row is the one to reach for most often. Append `.md` to any page's URL and you get
its source, so this page is at
`https://docs.tandemcx.ai/docs/overview/building-with-ai.md`. Every page also carries
a **Copy Markdown** button, with **View as Markdown**, **Open in ChatGPT**, and **Open in
Claude** behind it.

Reach for `llms.txt` when your assistant needs to find the right page, and a single `.md`
when you already know which page that is. `llms-full.txt` is the whole corpus, which is large
enough that it is usually the wrong choice for a single question.

### Tips

- **Point at a specific page rather than the whole corpus.** A single `.md` file answers a
  question with far less context than `llms-full.txt`.
- **Let it read your configuration before it proposes a change.** `list_voice_agents` then
  `get_voice_agent` gives an assistant the actual agent, so its suggestions apply to what you
  have rather than to a generic one.
- **Never paste a token into a prompt.** Put it in an environment variable and let the client
  send the header, so the token stays out of your conversation history.
- **Keep a token per tool.** Revoking one then costs you that tool's connection and nothing
  else.

## Next steps

<Cards>
    <Card
        title="Voice Agents"
        href="/docs/agents/voice-agents"
        description="What a voice agent is and what you configure."
    />
    <Card
        title="Build your first agent"
        href="/docs/agents/voice-agents/getting-started"
        description="From an empty form to a real call, in about ten minutes."
    />
    <Card
        title="Workflows"
        href="/docs/workflows"
        description="Build, test, and publish call and message flows."
    />
    <Card
        title="Knowledge Base"
        href="/docs/agents/knowledge-base/overview"
        description="Give an agent your own content to answer from."
    />
</Cards>
